// Carbone Word Template Manager - Frontend // Use same host as the page, just different port const CARBONE_API = window.location.protocol + '//' + window.location.hostname + ':8771/api/carbone'; // ============================================================ // NAVIGATION // ============================================================ // Load carbone templates when the templates page is shown const carboneNavObserver = new MutationObserver(() => { if (document.getElementById('page-templates') && document.getElementById('page-templates').classList.contains('active')) { loadCarboneTemplates(); } }); document.addEventListener('DOMContentLoaded', () => { const templatesPage = document.getElementById('page-templates'); if (templatesPage) carboneNavObserver.observe(templatesPage, { attributes: true, attributeFilter: ['class'] }); }); // ============================================================ // DOWNLOAD MASTER TEMPLATE // ============================================================ function downloadMasterTemplate() { window.open(CARBONE_API + '/master-template', '_blank'); } // ============================================================ // UPLOAD CUSTOM TEMPLATE // ============================================================ const carboneUploadZone = document.getElementById('carbone-upload-zone'); if (carboneUploadZone) { carboneUploadZone.addEventListener('dragover', e => { e.preventDefault(); carboneUploadZone.classList.add('dragover'); }); carboneUploadZone.addEventListener('dragleave', () => carboneUploadZone.classList.remove('dragover')); carboneUploadZone.addEventListener('drop', e => { e.preventDefault(); carboneUploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length) uploadCarboneTemplate(e.dataTransfer.files[0]); }); } async function uploadCarboneTemplate(file) { if (!file) return; const name = document.getElementById('carbone-template-name').value || file.name.replace(/\.[^.]+$/, ''); const status = document.getElementById('carbone-upload-status'); status.innerHTML = '
No custom templates uploaded yet. Download the master template, customize it in Word, and upload it here.
'; return; } list.innerHTML = data.templates.map(t => `Error loading templates: ' + e.message + '
'; } } // ============================================================ // RENDER SAMPLE CV (download PDF) // ============================================================ async function renderSampleCV(templateId) { toast('Generating sample CV PDF...'); // Get first candidate for sample data try { const candidatesResp = await fetch('/api/candidates?limit=1'); const candidatesData = await candidatesResp.json(); let cvData = null; if (candidatesData.candidates && candidatesData.candidates.length > 0) { const fullResp = await fetch('/api/candidates/' + candidatesData.candidates[0].id); cvData = await fullResp.json(); } if (!cvData) { toast('Upload a CV first to generate a sample', 'error'); return; } const resp = await fetch(CARBONE_API + '/templates/' + templateId + '/render', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ candidateData: cvData }) }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.error || 'Render failed'); } const blob = await resp.blob(); const url = URL.createObjectURL(blob); window.open(url, '_blank'); toast('Sample CV generated'); } catch (e) { toast('Render failed: ' + e.message, 'error'); } } // ============================================================ // PREVIEW TEMPLATE (inline HTML preview) // ============================================================ async function previewTemplate(templateId) { toast('Generating preview...'); try { // Get first candidate for sample data const candidatesResp = await fetch('/api/candidates?limit=1'); const candidatesData = await candidatesResp.json(); let cvData = null; if (candidatesData.candidates && candidatesData.candidates.length > 0) { const fullResp = await fetch('/api/candidates/' + candidatesData.candidates[0].id); cvData = await fullResp.json(); } if (!cvData) { toast('Upload a CV first to preview', 'error'); return; } const resp = await fetch(CARBONE_API + '/templates/' + templateId + '/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ candidateData: cvData }) }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.error || 'Preview failed'); } const html = await resp.text(); // Wrap the LibreOffice HTML in a styled container that adapts to the app theme. // LibreOffice exports plain HTML with inline styles — we inject a wrapper // that sets a white page background with padding so it reads like a document, // regardless of whether the app is in dark or light mode. const styledHtml = html.replace('