// templates-browser.js — extracted from templates_browser.html
// Data injected via window.__DATA__
let templates = [];
let allCategories = [];
let currentCategory = 'all';
let searchQuery = '';
async function fetchTemplates() {
const params = new URLSearchParams();
if (currentCategory !== 'all') params.set('category', currentCategory);
if (searchQuery) params.set('q', searchQuery);
try {
const resp = await fetch(`/api/v2/templates/?${params}`);
const data = await resp.json();
templates = data.templates;
allCategories = data.categories || [];
renderCategoryFilters();
renderGrid();
document.getElementById('templateCount').textContent =
`${data.total} template${data.total !== 1 ? 's' : ''} available`;
} catch (err) {
document.getElementById('templateGrid').innerHTML =
'<div class="empty-state"><p>Failed to load templates. Please try again.</p></div>';
}
}
function renderCategoryFilters() {
const container = document.getElementById('categoryFilters');
if (!allCategories.length) return;
container.innerHTML = allCategories.map(cat => {
const isActive = currentCategory === cat ? ' active' : '';
return `<button class="filter-btn${isActive}" data-category="${cat}">${cat}</button>`;
}).join('');
container.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
currentCategory = btn.dataset.category;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelector('[data-category="all"]').classList.remove('active');
fetchTemplates();
});
});
}
function renderGrid() {
const grid = document.getElementById('templateGrid');
if (!templates.length) {
grid.innerHTML = '<div class="empty-state"><p>No templates found matching your search.</p></div>';
return;
}
// Sort: featured first, then alphabetical
const sorted = [...templates].sort((a, b) => {
if (a.is_featured && !b.is_featured) return -1;
if (!a.is_featured && b.is_featured) return 1;
return a.name.localeCompare(b.name);
});
grid.innerHTML = sorted.map(t => {
const featuredClass = t.is_featured ? ' featured' : '';
let metaBadges = `<span class="badge">${t.field_count} fields</span>`;
if (t.steps > 1) metaBadges += `<span class="badge multi-step">${t.steps} steps</span>`;
if (t.has_conditionals) metaBadges += `<span class="badge conditional">Conditionals</span>`;
if (t.is_featured) metaBadges += `<span class="badge featured-badge">★ Featured</span>`;
const icon = t.icon || '📋';
return `
<div class="template-card${featuredClass}" data-slug="${t.slug}">
<div class="card-header">
<h3><span class="icon">${icon}</span>${t.name}</h3>
</div>
<div class="category-badge">${t.category}</div>
<div class="description">${t.description}</div>
<div class="meta">${metaBadges}</div>
<div class="actions">
<button class="btn btn-outline preview-btn">Preview</button>
<button class="btn btn-primary use-btn">Use Template</button>
</div>
</div>`;
}).join('');
// Wire up buttons via event delegation on the grid
grid.querySelectorAll('.preview-btn').forEach(btn => {
const slug = btn.closest('.template-card').dataset.slug;
btn.addEventListener('click', () => previewTemplate(slug));
});
grid.querySelectorAll('.use-btn').forEach(btn => {
const slug = btn.closest('.template-card').dataset.slug;
btn.addEventListener('click', () => cloneTemplate(slug));
});
// Wire up close button (in case modal is still open from previous state)
const initialCloseBtn = document.querySelector('#modalContent .close-btn');
if (initialCloseBtn) {
initialCloseBtn.addEventListener('click', closeModal);
}
}
async function previewTemplate(slug) {
const modal = document.getElementById('previewModal');
const content = document.getElementById('modalContent');
content.innerHTML = '<button class="close-btn" data-onclick="closeModal()" style="position:absolute;top:16px;right:16px;background:none;border:none;font-size:22px;cursor:pointer;color:#888;">×</button><div class="loading"><div class="spinner"></div></div>';
modal.classList.add('active');
try {
const resp = await fetch(`/api/v2/templates/${slug}`);
const data = await resp.json();
// Group by steps
const steps = {};
data.field_config.forEach(f => {
const step = f.step || 1;
if (!steps[step]) steps[step] = [];
steps[step].push(f);
});
let stepsHtml = '';
if (Object.keys(steps).length > 1) {
let groupsHtml = '';
Object.keys(steps).sort((a,b) => a-b).forEach(stepNum => {
const stepTitle = steps[stepNum][0].step_label || `Step ${stepNum}`;
const fieldNames = steps[stepNum].map(f => f.label).join(', ');
groupsHtml += `<div class="step-group"><div class="step-title">${stepTitle}</div><div class="step-fields">${fieldNames}</div></div>`;
});
stepsHtml = `<div class="steps-section"><h4>Multi-Step Form</h4>${groupsHtml}</div>`;
}
let fieldsHtml = data.field_config.map(f => {
let badges = '';
if (f.required) badges += '<span class="field-badge required">Required</span>';
if (f.condition) badges += '<span class="field-badge conditional">Conditional</span>';
return `
<div class="field-item">
<span class="field-label">${f.label}</span>
<span class="field-meta">
${f.type}${badges}
${f.options ? `<span class="field-badge">${f.options.length} options</span>` : ''}
</span>
</div>`;
}).join('');
content.innerHTML = `
<button class="close-btn" data-onclick="closeModal()" style="position:absolute;top:16px;right:16px;background:none;border:none;font-size:22px;cursor:pointer;color:#888;">×</button>
<h2>${data.icon || '📋'} ${data.name}</h2>
<div class="description">${data.description}</div>
<div class="meta" style="margin-bottom:16px;">
<span class="badge" style="padding:2px 10px;background:#f0f4ff;color:#1a5276;border-radius:12px;font-size:11px;font-weight:600;">${data.category}</span>
<span class="badge" style="padding:2px 8px;background:#f5f5f5;border-radius:12px;font-size:11px;">${data.field_config.length} fields</span>
</div>
${stepsHtml}
<h4 style="font-size:14px;margin-bottom:8px;">Fields</h4>
<div class="field-list">${fieldsHtml}</div>
${data.success_message ? `<p style="font-size:13px;color:#888;font-style:italic;margin-bottom:16px;">Confirmation: "${data.success_message}"</p>` : ''}
<div class="actions">
<button class="btn btn-outline modal-close-btn">Close</button>
<button class="btn btn-primary modal-use-btn">Use Template</button>
</div>
`;
// Wire up modal buttons
content.querySelector('.modal-close-btn').addEventListener('click', closeModal);
content.querySelector('.close-btn').addEventListener('click', closeModal);
content.querySelector('.modal-use-btn').addEventListener('click', () => {
cloneTemplate(data.slug);
closeModal();
});
} catch (err) {
content.innerHTML = '<p>Failed to load template details.</p>';
}
}
function closeModal() {
document.getElementById('previewModal').classList.remove('active');
}
async function cloneTemplate(slug) {
try {
const resp = await fetch(`/api/v2/templates/${slug}/clone`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (resp.status === 403) {
showToast('Site limit reached. Upgrade to create more forms.', true);
return;
}
const data = await resp.json();
if (data.status === 'created') {
showToast(`Form "${data.site.name}" created! Redirecting...`);
setTimeout(() => {
window.location.href = '/sites';
}, 1200);
}
} catch (err) {
showToast('Failed to create form. Please try again.', true);
}
}
function showToast(message, isError = false) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast' + (isError ? ' error' : '');
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
// "All Templates" button
document.querySelector('[data-category="all"]').addEventListener('click', () => {
currentCategory = 'all';
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
document.querySelector('[data-category="all"]').classList.add('active');
fetchTemplates();
});
// Search
let searchTimeout;
document.getElementById('searchBox').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchQuery = e.target.value.trim();
searchTimeout = setTimeout(fetchTemplates, 300);
});
// Modal close on overlay click
document.getElementById('previewModal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
// Initial load
fetchTemplates();