/** * A4 Template Designer using Gridstack.js * * Features: * - Drag-and-drop block placement on an exact A4 (794x1123px) canvas * - 12-column grid with row height of 10px for fine positioning * - Blocks can be moved and resized within canvas boundaries * - Save exports the clean JSON Template Schema * - Live preview with real candidate data * - PDF rendering via the Puppeteer backend */ // Block type definitions (palette items) const BLOCK_TYPES = { PersonalDetails: { icon: '♟', label: 'Header', desc: 'Name, contact, title', color: '#2c3e50', defaultW: 12, defaultH: 8 }, ProfessionalSummary: { icon: '✎', label: 'Summary', desc: 'Professional summary', color: '#3498db', defaultW: 12, defaultH: 5 }, WorkExperience: { icon: '⚙', label: 'Work Experience', desc: 'Job history list', color: '#27ae60', defaultW: 8, defaultH: 20, config: { showAchievements: true, showSkillsUsed: false, maxItems: null } }, SkillsList: { icon: '★', label: 'Skills', desc: 'Skills by category', color: '#e67e22', defaultW: 4, defaultH: 15, config: { groupByCategory: true, showYears: true, showProficiency: false, sidebar: true, maxItems: null } }, Education: { icon: '⛩', label: 'Education', desc: 'Degrees & institutions', color: '#8e44ad', defaultW: 4, defaultH: 10, config: { showDates: true, showField: true, showGrade: false } }, Certifications: { icon: '✅', label: 'Certifications', desc: 'Professional certs', color: '#e74c3c', defaultW: 4, defaultH: 8, config: { showDates: true, showIssuer: true } }, CustomText: { icon: '📝', label: 'Custom Text', desc: 'Free-form text block', color: '#95a5a6', defaultW: 12, defaultH: 5, config: { title: '', content: '' } }, Footer: { icon: '⚙', label: 'Footer', desc: 'Footer text', color: '#bdc3c7', defaultW: 12, defaultH: 3, config: { content: 'Generated by CV Application' } } }; // Designer state let designerGrid = null; let designerBlocks = []; let designerEditingTemplateId = null; let blockCounter = 0; // ============================================================ // INITIALIZATION // ============================================================ function showDesigner(templateId = null) { designerEditingTemplateId = templateId; designerBlocks = []; blockCounter = 0; // Load template if editing let loadPromise = Promise.resolve(); let templateName = 'New Template'; let templateDesc = ''; if (templateId) { loadPromise = api('/api/templates/' + templateId).then(t => { const structure = typeof t.template_structure === 'string' ? JSON.parse(t.template_structure) : t.template_structure; if (structure && structure.blocks) { designerBlocks = structure.blocks.map(b => ({...b})); blockCounter = designerBlocks.length; } templateName = t.name || templateName; templateDesc = t.description || ''; }); } loadPromise.then(() => { const body = `