Loading card editor...

Visiting Card Editor

Dashboard
FRONT
BACK
'; var w = window.open('', '_blank'); if (!w) { showToast('Allow popups to print', 'error'); return; } w.document.write(html); w.document.close(); // Auto-print after fonts load (1.5s buffer) setTimeout(function() { if (w && !w.closed) w.print(); }, 1500); } // ═══════════════════════════════════════════════════════════ // UTILS // ═══════════════════════════════════════════════════════════ // ═══ MOBILE FLOATING SIDEBAR — same as cv-builder ═══ if (window.innerWidth < 768) { document.addEventListener('DOMContentLoaded', function() { var sidebar = document.querySelector('.sidebar'); if (!sidebar) return; var isLight = document.documentElement.getAttribute('data-theme') === 'light'; var sidebarBg = isLight ? '#F8F8FC' : '#05050A'; function closeSidebar() { sidebar.style.cssText = 'display:none !important'; if (sidebar.parentNode !== sidebarParent) sidebarParent.appendChild(sidebar); ov.style.display = 'none'; document.body.style.overflow = ''; } function openSidebar() { if (sidebar.parentNode !== document.body) document.body.appendChild(sidebar); sidebar.style.cssText = 'display:block !important;position:fixed !important;left:0 !important;top:56px !important;bottom:56px !important;width:85vw !important;max-width:320px !important;z-index:1000 !important;max-height:none !important;background:' + sidebarBg + ' !important;border-right:2px solid rgba(124,106,255,0.3) !important;overflow-y:auto !important;-webkit-overflow-scrolling:touch !important;overscroll-behavior:contain !important;padding:16px 16px 20px !important;box-shadow:8px 0 40px rgba(0,0,0,0.5) !important'; ov.style.display = 'block'; document.body.style.overflow = 'hidden'; } var sidebarParent = sidebar.parentNode; sidebar.style.cssText = 'display:none !important'; // Close button inside sidebar var closeBtn = document.createElement('button'); closeBtn.style.cssText = 'position:absolute;top:12px;right:12px;width:36px;height:36px;border-radius:8px;background:none;border:1px solid rgba(124,106,255,0.3);color:inherit;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;z-index:10'; closeBtn.innerHTML = ''; closeBtn.onclick = closeSidebar; sidebar.insertBefore(closeBtn, sidebar.firstChild); sidebar.addEventListener('click', function(e) { e.stopPropagation(); }); sidebar.addEventListener('touchstart', function(e) { e.stopPropagation(); }, { passive: true }); sidebar.addEventListener('touchmove', function(e) { e.stopPropagation(); }, { passive: true }); // Overlay var ov = document.createElement('div'); ov.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;z-index:999;background:rgba(0,0,0,0.4)'; ov.onclick = function(e) { if (e.target === ov) closeSidebar(); }; document.body.appendChild(ov); // Floating toggle button var btn = document.createElement('button'); btn.style.cssText = 'position:fixed;bottom:70px;right:12px;z-index:500;width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#7C6AFF,#00D4AA);color:#fff;border:none;cursor:pointer;font-size:22px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 20px rgba(124,106,255,0.4);touch-action:manipulation;'; btn.innerHTML = ''; btn.onclick = function(e) { e.stopPropagation(); e.preventDefault(); openSidebar(); }; document.body.appendChild(btn); new MutationObserver(function() { isLight = document.documentElement.getAttribute('data-theme') === 'light'; sidebarBg = isLight ? '#F8F8FC' : '#05050A'; }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); }); } function showToast(msg, type) { var existing = document.getElementById('toast-msg'); if (existing) existing.remove(); var toast = document.createElement('div'); toast.id = 'toast-msg'; toast.className = 'toast'; var colors = { error: '#FF4444', success: '#00D4AA', upgrade: '#FF6AC2', info: '#7C6AFF' }; toast.style.background = colors[type] || '#7C6AFF'; var icons = { error: 'ph-warning-circle', success: 'ph-check-circle', upgrade: 'ph-crown', info: 'ph-info' }; toast.innerHTML = '' + msg + ''; document.body.appendChild(toast); setTimeout(function() { if (toast.parentNode) toast.remove(); }, 4000); } function setTheme(t) { document.documentElement.setAttribute('data-theme', t); localStorage.setItem('cvTheme', t); var i = document.getElementById('themeIcon'); if(i) i.className = t === 'light' ? 'ph ph-moon' : 'ph ph-sun'; } function toggleTheme() { setTheme(document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light'); } setTheme(localStorage.getItem('cvTheme') || 'dark'); // ═══════════════════════════════════════════════════════════ // GUIDE SYSTEM — draggable padding lines on card preview // ═══════════════════════════════════════════════════════════ var GUIDE_DEFAULTS = { classic: { left:20, right:20, textTop:22, gap:20 }, modern: { left:26, right:26, textTop:22, gap:22 }, minimal: { left:32, right:32, textTop:28, gap:22 }, bold: { left:24, right:24, textTop:22, gap:18 }, corporate: { left:28, right:28, textTop:20, gap:0 }, creative: { left:28, right:28, textTop:24, gap:20 } }; var guideState = { enabled:false, left:26, right:26, textTop:22, gap:22, dragging:null, startX:0, startY:0, startVal:0, scaleX:1, scaleY:1 }; function toggleGuides() { guideState.enabled = !guideState.enabled; var btn=document.getElementById('guidesBtn'); if(btn){btn.style.borderColor=guideState.enabled?'#7C6AFF':'';btn.style.color=guideState.enabled?'#7C6AFF':'';btn.style.background=guideState.enabled?'rgba(124,106,255,0.1)':'';} var sb=document.getElementById('guideSaveBtn'); if(sb) sb.style.display=guideState.enabled?'flex':'none'; if(!guideState.enabled){var ov=document.getElementById('guide-overlay');if(ov)ov.remove();} else renderGuides(); } function renderGuides() { var front=document.getElementById('cardFront'); if(!front) return; var ex=document.getElementById('guide-overlay'); if(ex)ex.remove(); if(!guideState.enabled) return; var W=630,H=360; var l=guideState.left, r=guideState.right, t=guideState.textTop, gap=guideState.gap; var rx=W-r; var photoSize=parseInt((document.getElementById('photoSize')||{value:72}).value)||72; // Photo bounding box — tracks actual dragged position var pL=l+photoPos.x; var pR=l+photoSize+photoPos.x; var pT=(H-photoSize)/2+photoPos.y; var pB=(H+photoSize)/2+photoPos.y; var pCX=l+photoSize/2+photoPos.x; var gx=l+photoSize+gap; // text column left edge var hasGap=(selectedTemplate!=='classic'&&selectedTemplate!=='corporate'); var ov=document.createElement('div'); ov.id='guide-overlay'; ov.style.cssText='position:absolute;top:0;left:0;width:100%;height:100%;z-index:9000;pointer-events:none;'; function ln(x1,y1,x2,y2,c){return '';} function hd(cx,cy,label,id,cursor,c){ var hw=Math.max(46,label.length*7+18); return '' +'' +''+label+'' +''; } ov.innerHTML='' +'' // Left outer — purple +ln(l,0,l,H,'#7C6AFF') +hd(l,H*0.78,l+'px','gh-left','ew-resize','#7C6AFF') // ── PHOTO BOUNDING BOX — orange (all 4 sides drag to resize) ── // Photo left edge +ln(pL,pT,pL,pB,'#F6AD55') +hd(pL,(pT+pB)/2,photoSize+'px','gh-photo-l','ew-resize','#F6AD55') // Photo right edge +ln(pR,pT,pR,pB,'#F6AD55') +hd(pR,(pT+pB)/2,photoSize+'px','gh-photo-r','ew-resize','#F6AD55') // Photo top edge +ln(pL,pT,pR,pT,'#F6AD55') +hd(pCX,pT,photoSize+'px','gh-photo-t','ns-resize','#F6AD55') // Photo bottom edge +ln(pL,pB,pR,pB,'#F6AD55') +hd(pCX,pB,photoSize+'px','gh-photo-b','ns-resize','#F6AD55') // Gap (text column left edge) — teal +(hasGap ? ln(gx,0,gx,H,'#00D4AA')+hd(gx,H*0.78,'gap '+gap+'px','gh-gap','ew-resize','#00D4AA') : '') // Right outer — purple +ln(rx,0,rx,H,'#7C6AFF') +hd(rx,H*0.78,r+'px','gh-right','ew-resize','#7C6AFF') // Top — pink +ln(0,t,W,t,'#FF6AC2') +hd(W/2,t,t+'px top','gh-top','ns-resize','#FF6AC2') // Bottom — pink +ln(0,H-t,W,H-t,'#FF6AC2') +hd(W/2,H-t,t+'px btm','gh-bottom','ns-resize','#FF6AC2') +''; front.style.position='relative'; front.appendChild(ov); function bind(id,g){var el=document.getElementById(id);if(el)el.addEventListener('mousedown',function(e){startGuideDrag(e,g);},{passive:false});} bind('gh-left','left'); bind('gh-right','right'); bind('gh-gap','gap'); bind('gh-photo-l','photo-l'); bind('gh-photo-r','photo-r'); bind('gh-photo-t','photo-t'); bind('gh-photo-b','photo-b'); bind('gh-top','top'); bind('gh-bottom','bottom'); } function startGuideDrag(e,guide) { var frame=document.getElementById('cardFront'), rect=frame.getBoundingClientRect(); guideState.scaleX=630/rect.width; guideState.scaleY=360/rect.height; guideState.dragging=guide; guideState.startX=e.clientX; guideState.startY=e.clientY; var ps=parseInt((document.getElementById('photoSize')||{value:72}).value)||72; var isPhoto=guide==='photo-l'||guide==='photo-r'||guide==='photo-t'||guide==='photo-b'; guideState.startVal=guide==='left'?guideState.left:guide==='right'?guideState.right:guide==='gap'?guideState.gap:isPhoto?ps:guideState.textTop; e.preventDefault(); e.stopPropagation(); document.addEventListener('mousemove',onGuideDrag); document.addEventListener('mouseup',stopGuideDrag); } function onGuideDrag(e) { if(!guideState.dragging) return; var g=guideState.dragging,d; if(g==='left'){d=Math.round((e.clientX-guideState.startX)*guideState.scaleX);guideState.left=Math.max(0,Math.min(120,guideState.startVal+d));} else if(g==='right'){d=Math.round((guideState.startX-e.clientX)*guideState.scaleX);guideState.right=Math.max(0,Math.min(120,guideState.startVal+d));} else if(g==='gap'){d=Math.round((e.clientX-guideState.startX)*guideState.scaleX);guideState.gap=Math.max(0,Math.min(80,guideState.startVal+d));} else if(g==='photo-r'||g==='photo-l'||g==='photo-t'||g==='photo-b'){ if(g==='photo-r') d=Math.round((e.clientX-guideState.startX)*guideState.scaleX); else if(g==='photo-l') d=Math.round((guideState.startX-e.clientX)*guideState.scaleX); else if(g==='photo-t') d=Math.round((guideState.startY-e.clientY)*guideState.scaleY); else d=Math.round((e.clientY-guideState.startY)*guideState.scaleY); var np=Math.max(40,Math.min(140,guideState.startVal+d)); var sl=document.getElementById('photoSize'); if(sl){sl.value=np;var sv=document.getElementById('photoSizeVal');if(sv)sv.textContent=np+'px';} } else{d=g==='top'?Math.round((e.clientY-guideState.startY)*guideState.scaleY):Math.round((guideState.startY-e.clientY)*guideState.scaleY);guideState.textTop=Math.max(0,Math.min(80,guideState.startVal+d));} updatePreview(); } function stopGuideDrag() { guideState.dragging=null; document.removeEventListener('mousemove',onGuideDrag); document.removeEventListener('mouseup',stopGuideDrag); } function setCardMode(mode) { cardMode = mode; var dBtn=document.getElementById('modeBtnDouble'), sBtn=document.getElementById('modeBtnSingle'); if(dBtn&&sBtn){ dBtn.style.background=mode==='double'?'#7C6AFF':'rgba(255,255,255,0.04)'; dBtn.style.color=mode==='double'?'#fff':'var(--text-muted)'; sBtn.style.background=mode==='single'?'#7C6AFF':'rgba(255,255,255,0.04)'; sBtn.style.color=mode==='single'?'#fff':'var(--text-muted)'; } updatePreview(); } async function quickSave() { var o = getOpts(); var payload = { version_name: 'My Card', cv_id: selectedCvId, template: o.template, primary_color: o.primaryColor, bg_color: o.bgColor, text_color: o.textColor, font: o.font, show_photo: o.showPhoto, photo_shape: o.photoShape, photo_size: o.photoSize, show_qr: o.showQR, show_barcode: o.showBarcode, show_designation: o.showDesignation, show_company: o.showCompany, card_mode: cardMode, fs_name: fontSize.name, fs_title: fontSize.title, fs_contact: fontSize.contact }; try { // Update active card or create new one var activeCard = savedVersions.find(function(v){return v.is_active;}); var res; if (activeCard) { payload.version_name = activeCard.version_name; res = await fetch('/api/visiting-cards/'+activeCard.id, { method:'PUT', headers:{'x-user-id':userId,'Content-Type':'application/json'}, body:JSON.stringify(payload) }); } else { res = await fetch('/api/visiting-cards', { method:'POST', headers:{'x-user-id':userId,'Content-Type':'application/json'}, body:JSON.stringify(payload) }); var d = res.ok ? await res.json() : null; if (d && d.card) { await fetch('/api/visiting-cards/'+d.card.id+'/activate', {method:'PUT',headers:{'x-user-id':userId}}); } loadVersions(); showToast('Card saved & activated!','success'); return; } if (res.ok) { // Also activate await fetch('/api/visiting-cards/'+activeCard.id+'/activate', {method:'PUT',headers:{'x-user-id':userId}}); showToast('Card saved!','success'); loadVersions(); } } catch(e) { showToast('Save failed','error'); } } function attachPhotoDrag() { var wrap=document.getElementById('photo-drag-wrap'); if(!wrap) return; wrap.addEventListener('mousedown',function(e){ var frame=document.getElementById('cardFront'),rect=frame.getBoundingClientRect(); var sx=630/rect.width,sy=360/rect.height; var startX=e.clientX,startY=e.clientY,startPX=photoPos.x,startPY=photoPos.y; wrap.style.cursor='grabbing'; e.preventDefault();e.stopPropagation(); function onMove(e){var nx=startPX+Math.round((e.clientX-startX)*sx),ny=startPY+Math.round((e.clientY-startY)*sy);wrap.style.transform='translate('+nx+'px,'+ny+'px)';} function onUp(e){photoPos.x=startPX+Math.round((e.clientX-startX)*sx);photoPos.y=startPY+Math.round((e.clientY-startY)*sy);wrap.style.cursor='grab';document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);if(guideState.enabled)renderGuides();} document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp); }); } function saveGuideSettings() { var tplNames={classic:'Executive',modern:'Luxury Noir',minimal:'Minimal Pro',bold:'Tech Carbon',corporate:'Royal Navy',creative:'Creative'}; localStorage.setItem('cc_guide_'+selectedTemplate, JSON.stringify({left:guideState.left,right:guideState.right,textTop:guideState.textTop,gap:guideState.gap})); showToast('Saved padding for '+(tplNames[selectedTemplate]||selectedTemplate),'success'); } // ═══════════════════════════════════════════════════════════ // VISITING CARD VERSIONS // ═══════════════════════════════════════════════════════════ var savedVersions = []; var activeVersionId = null; var TPL_NAMES = { classic:'Executive', modern:'Luxury Noir', minimal:'Minimal Pro', bold:'Tech Carbon', corporate:'Royal Navy', creative:'Creative' }; async function loadVersions() { try { var res = await fetch('/api/visiting-cards', { headers: { 'x-user-id': userId } }); if (!res.ok) return; var data = await res.json(); savedVersions = data.cards || []; activeVersionId = (savedVersions.find(function(v){return v.is_active;})||{}).id || null; renderVersionsList(); // Auto-apply active version on page load var active = savedVersions.find(function(v){return v.is_active;}); if (active) applyVersionSettings(active, false); // false = don't show toast } catch(e) {} } function applyVersionSettings(v, showMsg) { // Template selectedTemplate = v.template || 'classic'; document.querySelectorAll('.template-thumb').forEach(function(el){ el.classList.toggle('active', el.dataset.tpl === selectedTemplate); }); // Colors + font document.getElementById('colorPrimary').value = v.primary_color || '#7C6AFF'; document.getElementById('colorBg').value = v.bg_color || '#F8F8F8'; document.getElementById('colorText').value = v.text_color || '#0D0D14'; document.getElementById('fontSelector').value = v.font || 'Inter'; // Toggles document.getElementById('togglePhoto').checked = v.show_photo !== false; document.getElementById('toggleQR').checked = v.show_qr !== false; document.getElementById('toggleBarcode').checked = v.show_barcode !== false; document.getElementById('toggleDesignation').checked = v.show_designation !== false; document.getElementById('toggleCompany').checked = v.show_company !== false; // Photo size + shape var ps = document.getElementById('photoSize'); if (ps) { ps.value = v.photo_size||140; var sv=document.getElementById('photoSizeVal'); if(sv)sv.textContent=(v.photo_size||80)+'px'; } document.querySelectorAll('[data-shape]').forEach(function(el){ el.classList.toggle('active', el.dataset.shape===(v.photo_shape||'circle')); }); // Card mode (single/double) if (v.card_mode) setCardMode(v.card_mode); // Guide padding — load from localStorage or template defaults var gd = GUIDE_DEFAULTS[selectedTemplate] || GUIDE_DEFAULTS.classic; var saved = null; try { saved = JSON.parse(localStorage.getItem('cc_guide_'+selectedTemplate)||'null'); } catch(e){} var gs = saved || gd; guideState.left=gs.left||gd.left; guideState.right=gs.right||gd.right; guideState.textTop=gs.textTop||gd.textTop; guideState.gap=gs.gap!=null?gs.gap:gd.gap; // Font sizes (restore if saved) function setFsSlider(id,valId,val){var el=document.getElementById(id);var vl=document.getElementById(valId);if(el&&val){el.value=val;if(vl)vl.textContent=val+'px';}} if(v.fs_name){fontSize.name=v.fs_name;setFsSlider('fsName','fsNameVal',v.fs_name);} if(v.fs_title){fontSize.title=v.fs_title;setFsSlider('fsTitle','fsTitleVal',v.fs_title);} if(v.fs_contact){fontSize.contact=v.fs_contact;setFsSlider('fsContact','fsContactVal',v.fs_contact);} // Reset photo position photoPos.x = 0; photoPos.y = 0; updatePreview(); if (showMsg !== false) showToast('Loaded: '+esc(v.version_name), 'success'); } function renderVersionsList() { var el = document.getElementById('versionsList'); if (!el) return; if (!savedVersions.length) { el.innerHTML = '
No saved versions yet.
Design your card and save it.
'; return; } el.innerHTML = savedVersions.map(function(v) { return '
' +'
' +''+esc(v.version_name)+'' +(v.is_active?'✓ Active':'') +'
' +'
'+(TPL_NAMES[v.template]||v.template)+'
' +'
' +'' +'' +(!v.is_active?'':'') +'' +'
'; }).join(''); } function showSaveModal() { document.getElementById('verModalName').value = 'My Card ' + (savedVersions.length + 1); document.getElementById('verModal').style.display = 'flex'; setTimeout(function(){ document.getElementById('verModalName').select(); }, 100); } function closeVerModal() { document.getElementById('verModal').style.display = 'none'; } function confirmSaveVersion() { var name = document.getElementById('verModalName').value.trim(); if (!name) return; closeVerModal(); saveVersion(name); } document.addEventListener('keydown', function(e) { if (e.key === 'Enter' && document.getElementById('verModal').style.display !== 'none') confirmSaveVersion(); if (e.key === 'Escape') closeVerModal(); }); async function saveVersion(name) { try { var o = getOpts(); var res = await fetch('/api/visiting-cards', { method: 'POST', headers: { 'x-user-id': userId, 'Content-Type': 'application/json' }, body: JSON.stringify({ version_name: name, cv_id: selectedCvId, template: o.template, primary_color: o.primaryColor, bg_color: o.bgColor, text_color: o.textColor, font: o.font, show_photo: o.showPhoto, photo_shape: o.photoShape, photo_size: o.photoSize, show_qr: o.showQR, show_barcode: o.showBarcode, show_designation: o.showDesignation, show_company: o.showCompany, card_mode: cardMode, fs_name: fontSize.name, fs_title: fontSize.title, fs_contact: fontSize.contact }) }); var data = await res.json(); if (!res.ok) { showToast(data.error || 'Save failed', 'error'); return; } savedVersions.unshift(data.card); renderVersionsList(); showToast('Design saved: ' + name, 'success'); } catch(e) { showToast('Save failed', 'error'); } } async function updateVersion(id) { var v = savedVersions.find(function(x){return x.id===id;}); if (!v) return; document.getElementById('verModalName').value = v.version_name; document.getElementById('verModal').style.display = 'flex'; document.getElementById('verModal').dataset.updateId = id; setTimeout(function(){ document.getElementById('verModalName').select(); }, 100); // Override confirmSave for update flow document.getElementById('verModal').dataset.mode = 'update'; } // Monkey-patch confirmSaveVersion to handle update mode var _origConfirm = confirmSaveVersion; confirmSaveVersion = function() { var modal = document.getElementById('verModal'); var name = document.getElementById('verModalName').value.trim(); if (!name) return; var mode = modal.dataset.mode; var updateId = modal.dataset.updateId; modal.dataset.mode = ''; modal.dataset.updateId = ''; closeVerModal(); if (mode === 'update' && updateId) { doUpdateVersion(updateId, name); } else { saveVersion(name); } }; async function doUpdateVersion(id, name) { try { var o = getOpts(); var res = await fetch('/api/visiting-cards/'+id, { method: 'PUT', headers: { 'x-user-id': userId, 'Content-Type': 'application/json' }, body: JSON.stringify({ version_name: name, template: o.template, primary_color: o.primaryColor, bg_color: o.bgColor, text_color: o.textColor, font: o.font, show_photo: o.showPhoto, photo_shape: o.photoShape, photo_size: o.photoSize, show_qr: o.showQR, show_barcode: o.showBarcode, show_designation: o.showDesignation, show_company: o.showCompany, card_mode: cardMode, fs_name: fontSize.name, fs_title: fontSize.title, fs_contact: fontSize.contact }) }); var data = await res.json(); if (!res.ok) { showToast(data.error || 'Update failed', 'error'); return; } var idx = savedVersions.findIndex(function(x){return x.id===id;}); if (idx >= 0) savedVersions[idx] = data.card; renderVersionsList(); showToast('Version updated!', 'success'); } catch(e) { showToast('Update failed', 'error'); } } function loadVersion(id) { var v = savedVersions.find(function(x){return x.id===id;}); if (v) applyVersionSettings(v, true); } async function activateVersion(id) { try { var res = await fetch('/api/visiting-cards/'+id+'/activate', { method: 'PUT', headers: { 'x-user-id': userId } }); if (!res.ok) { showToast('Failed', 'error'); return; } savedVersions.forEach(function(v) { v.is_active = (v.id===id); }); activeVersionId = id; renderVersionsList(); showToast('Set as active visiting card!', 'success'); } catch(e) { showToast('Failed', 'error'); } } async function deleteVersion(id) { if (!confirm('Delete this card version?')) return; try { var res = await fetch('/api/visiting-cards/'+id, { method: 'DELETE', headers: { 'x-user-id': userId } }); if (!res.ok) { showToast('Delete failed', 'error'); return; } savedVersions = savedVersions.filter(function(v){return v.id!==id;}); renderVersionsList(); showToast('Version deleted', 'success'); } catch(e) { showToast('Delete failed', 'error'); } }