`;
}
// ============================================================
// DRAG & DROP
// ============================================================
function canvasDragStart(idx) {
dragSourceIdx = idx;
const el = document.getElementById('section-' + idx);
if (el) el.classList.add('dragging');
}
function canvasDragOver(event, idx) {
event.preventDefault();
if (dragSourceIdx !== null && dragSourceIdx !== idx) {
document.querySelectorAll('.canvas-section').forEach(e => e.classList.remove('drag-over'));
const el = document.getElementById('section-' + idx);
if (el) el.classList.add('drag-over');
}
}
function canvasDrop(event, targetIdx) {
event.preventDefault();
const data = event.dataTransfer.getData('text/plain');
if (data && data.startsWith('{')) {
// Drop from palette
const parsed = JSON.parse(data);
if (parsed.action === 'add' && parsed.type) {
builderAddFromPalette(parsed.type);
// Move to the target position
const justAdded = builderSections.length - 1;
if (justAdded !== targetIdx) {
const item = builderSections.pop();
builderSections.splice(targetIdx, 0, item);
}
reorderSections();
renderCanvas();
renderPreview();
}
} else if (dragSourceIdx !== null && dragSourceIdx !== targetIdx) {
// Reorder
const item = builderSections.splice(dragSourceIdx, 1)[0];
builderSections.splice(targetIdx, 0, item);
reorderSections();
renderCanvas();
renderPreview();
}
dragSourceIdx = null;
}
function canvasDragEnd() {
document.querySelectorAll('.canvas-section').forEach(e => {
e.classList.remove('dragging', 'drag-over');
});
dragSourceIdx = null;
}
// ============================================================
// SECTION OPERATIONS
// ============================================================
function updateSectionName(idx, name) {
builderSections[idx].name = name;
renderPreview();
}
function updateStyling(idx, key, value) {
if (!builderSections[idx].styling) builderSections[idx].styling = {};
builderSections[idx].styling[key] = value;
renderPreview();
}
function moveSection(idx, dir) {
const newIdx = idx + dir;
if (newIdx < 0 || newIdx >= builderSections.length) return;
const item = builderSections.splice(idx, 1)[0];
builderSections.splice(newIdx, 0, item);
reorderSections();
renderCanvas();
renderPreview();
}
function deleteSection(idx) {
builderSections.splice(idx, 1);
reorderSections();
renderCanvas();
renderPreview();
}
function reorderSections() {
builderSections.forEach((s, i) => s.order = i + 1);
}
// ============================================================
// LIVE PREVIEW
// ============================================================
let sampleData = null;
function loadSamplePreviewData() {
// Use the first parsed candidate as sample data
api('/api/candidates?limit=1').then(data => {
if (data.candidates.length > 0) {
api('/api/candidates/' + data.candidates[0].id).then(c => {
sampleData = c;
renderPreview();
});
}
});
}
function renderPreview() {
const el = document.getElementById('builder-preview-cv');
if (!el) return;
if (!sampleData) {
el.innerHTML = '
Upload a CV to see live preview with real data
';
return;
}
const c = sampleData.candidate;
let html = '';
for (const section of builderSections) {
const st = section.styling || {};
switch (section.type) {
case 'header':
html += `
${c.first_name || ''} ${c.last_name || ''}
`;
const contactParts = [];
if (c.email) contactParts.push(c.email);
if (c.phone) contactParts.push(c.phone);
if (c.address) contactParts.push(c.address);
if (c.linkedin) contactParts.push(c.linkedin);
if (c.github) contactParts.push(c.github);
if (c.website) contactParts.push(c.website);
if (contactParts.length) html += `
${contactParts.join(' | ')}
`;
break;
case 'summary':
if (c.summary) html += `
${section.name}
${c.summary}
`;
break;
case 'experience':
let exps = sampleData.experience || [];
if (st.max_items) exps = exps.slice(0, st.max_items);
html += `
`;
if (st.show_years !== false) {
const start = e.start_date ? formatDate(e.start_date) : '';
const end = e.end_date ? formatDate(e.end_date) : 'Present';
html += `
${start} — ${end}
`;
}
if (e.description) html += `
${e.description}
`;
if (st.show_achievements !== false && e.achievements) {
const ach = typeof e.achievements === 'string' ? JSON.parse(e.achievements) : e.achievements;
if (Array.isArray(ach) && ach.length) {
html += '
' + ach.map(a => `
${a}
`).join('') + '
';
}
}
if (st.show_skills_used && e.skills_used) {
const skills = typeof e.skills_used === 'string' ? JSON.parse(e.skills_used) : e.skills_used;
if (Array.isArray(skills) && skills.length) {
html += `
Skills: ${skills.join(', ')}
`;
}
}
html += '
';
}
break;
case 'skills':
let skills = sampleData.skills || [];
if (st.max_items) skills = skills.slice(0, st.max_items);
html += `
${section.name}
`;
if (st.group_by_category) {
const groups = {};
for (const s of skills) {
const cat = s.skill_category || 'Other';
if (!groups[cat]) groups[cat] = [];
groups[cat].push(s);
}
for (const [cat, items] of Object.entries(groups)) {
html += `
${cat}: `;
html += items.map(s => {
let txt = `${s.skill_name}`;
if (st.show_proficiency && s.proficiency) txt += ` (${s.proficiency})`;
if (st.show_years && s.years_experience) txt += ` ${s.years_experience}y`;
txt += '';
return txt;
}).join('');
html += '