View as
Library
Analytics
Live Tracking
Playbook (Client)
Viewer (Client)
Good morning ☀️
Here's what needs your attention today
Preview buyer view
Active Deals
5
↑ 2 opened today
Buyer Views
12
↑ 4 in last hour
Nudges Pending
3
Action needed
Avg Engagement
68%
↑ vs last week
PS
Priya Sharma visited pricing sheet
FinCorp India · 2 min ago · 3rd visit today
HOT — 8 min on pricing
AM
Amit Mehta opened product deck
TechCorp Ltd · 1h ago · 6 min read
WARM — gone quiet 3 days
RK
Ravi Kumar opened playbook
Finserv Co · 5 days ago · dropped at p.2
⚪ COLD — re-engage
⚡ Actions for you
3 pending
Follow up with Priya — NOW
Viewed pricing 3x. 8 min on pricing page. Strike while hot.
Send nudge
View signals
6 min on product deck, quiet 3 days. Share a case study.
Send nudge
View signals
Dropped at page 2, 5 days ago. Try a new hook.
Send nudge
View signals
Active Playbooks
View all →
Q2 Enterprise Pitch
Priya Sharma · FinCorp India
72% engagement · 4 views
Product Walkthrough
Amit Mehta · TechCorp
45% engagement · 2 views
Finserv Intro Pack
Ravi Kumar · Finserv Co
18% engagement · 1 view
Collateral Library
Select collaterals · create a Sales Playbook
Upload
Interactive map
Clear
Quick
Share
Build Playbook
Collaterals
Playbooks 5
Type: All
PDF
Presentations
Videos
Images
Spreadsheets
Audience: All
Customer-facing
Customer + Learning
Internal · Learning
Read: All
Unread
⏳ Partial
Fully read
Assigned to me 2
Created by me 3
Playbooks shared
with you by others
Leads
People who engaged with your content — ranked
by intent
All
Hot
Warm
Cold
New
All sources
Playbook
Landing page
CRM
Manual
By intent
Most recent
Most views
CRM Sync
Add Lead
Deal Rooms
All active deal workspaces — click to open
Learn
Internal know-how from the Library — the same files, no copies
Manage in Library
AI Roleplay
Practise the next conversation with your real buyers before it happens
Calls
Logged and recorded sales calls with notes and buyer signals
Upload recording Log a call
Meetings
Scheduled and recorded meetings with briefs, bot notes and recaps
Schedule meeting
Emails
Email threads with buyers — including playbook shares sent by email
Compose email
Team & Module Management
Manage teams, assign modules, and control visibility for reps
and clients
New Team
Teams & Companies
Members
Module Config
Team Members
Manage member access and module visibility
Add Member
How it works: Core modules are always enabled. Optional modules can be toggled globally
here, then assigned to specific teams. You can also hide modules from individual reps or clients.
Demo Hub
Create interactive demo journeys · Share with tracking ·
Public gallery
New Demo
Public Gallery
Analytics
Engagement & reach — all content
All Content
Playbooks
Collaterals
Views per Day All content
Page Heatmap avg
seconds per page
Intensity:
None
Mid
Peak
Live Tracking
Real-time activity across all playbooks & collaterals
2
viewing now
Alerts
Live visitors 2 active
Recent (last 2 hrs)
Library
Playbook
Viewing
Rahul is online
P 1 / 5
1 Details
2 Content
3 Template
4 Builder
5 Security
Sequence 0 steps
Add step
Drag to reorder · Click to preview
No content yet
Click Add step to pick collaterals, paste a URL, or upload files.
Library
URL
Upload
Drive
Audio
Preview
Click a step to preview its content
No content selected
Add steps from the left and click one to preview it here
Pick a landing page template. You'll
customise it in the next step.
Drag to reorder
Add
blocks from above
Control who can access this playbook and for how long. Recipients see active restrictions on their
landing page.
Link expiry
How long the link stays active
No expiry
One time view
1 day
7 days
14 days
30 days
Custom date…
View limit
Max times this link can be opened
Unlimited
1 view
3 views
5 views
10 views
Viewer verification (OTP)
Public link
Block forwarding
Anyone can open
Watermark documents
No watermark
Allow recipient to share
Recipient can reshare
Playbook Sequence
Drag to reorder · Click × to remove · Click eye to
preview
Add more
Done
Analytics
Engagement overview
PS
Priya Sharma
Viewing page 3 · Q2 Product Deck · 6 min on doc
Online now
Auto-Nudge ON
Bot sends a help message to visitor — then pushes for a call or meeting
Visitor will see this message
automatically
Hi Priya! I see you're reviewing the product deck — — any questions? I'm available
for a quick call or can book a meeting at your convenience.
Call Rahul now
Book a meeting
Send nudge now
Share with Many
Filter by location, segment, engagement
Add recipients
Message (optional)
Link security
Viewer verification
Public
Share with Many
Filter contacts by location, engagement, segment
Location
Mumbai
Delhi
Bangalore
Pune
Chennai
Segment
Enterprise
Mid-Market
SMB
Industry
BFSI
Tech
Health
⭐ VIP
0 selected
Select all · Clear
playbook.link/share/...
Mobile
Admin
CTA Editor
0 CTAs
Save &
Close
Add
CTA:
Button
Buy Now
Video
Audio
Quiz
Form
Meeting
Hotspot
360°
Zones
Click any CTA type above to place it on the document. Then drag, resize, and configure it.
Click a CTA on
the page to edit it
Button / Link
Drag to move · handles to resize
Content
Style
Animation
Color
Solid
Gradient
Saved
→
↓
↘
↗
⊙
Add Color Stop
Apply
Gradient
Save
Gradient
Shape
Rounded
Pill
Square
Size
Small
Medium
Large
Icon
None
Saved Button Styles
Save current
Apply
Style
Motion
None Static
🫀 Pulse Glowing ring
🏀 Bounce Up-down float
📳 Shake Attention grab
✨ Glow Brightness flash
👋 Wiggle Rotate shake
Speed
Slow
Normal
Fast
Apply
Animation
🛒0
🏆
Excellent!
Get Certificate
View
Leaderboard
Try
Again
🏆
Leaderboard
All Time
This Week
My Rank
1
Choose Template
Upload your ownPNG or JPG background
2
Add Placeholders
Click any
placeholder to drop it on the certificate. Drag to reposition.
Recipient
Click fields on canvas to select · Drag to move
Click a placeholder button to
add it, then click it on the canvas to edit its font, size, and colour.
c.id === ctaId);
if (cta && cta.z) zmOpen(cta, collIdx, 'view');
}
function zmClose(force) {
if (!force && ZM.mode === 'edit' && ZM.dirty && !window.confirm('Close without saving your changes to the zones?')) return;
document.getElementById('zmOverlay').classList.remove('open');
const fr = document.getElementById('zmFrame'); ZM.frames.delete(fr); fr.srcdoc = '';
ZM.cta = null;
}
/* ── Live status channel ──
Today: in-page + BroadcastChannel (other tabs of the same browser).
For production, replace publish()/subscribe() with your realtime backend (WebSocket, Firebase, Supabase, …).
Payload is just { ctaId, s: { [regionKey]: { tag, text } } }. */
const zmLiveTransport = (() => {
const subs = new Set();
let bc = null; try { bc = new BroadcastChannel('zm-live'); bc.onmessage = e => subs.forEach(f => f(e.data)); } catch (e) {}
return {
publish(msg) { subs.forEach(f => f(msg)); try { bc && bc.postMessage(msg); } catch (e) {} },
subscribe(fn) { subs.add(fn); return () => subs.delete(fn); }
};
})();
zmLiveTransport.subscribe(msg => {
ZM.frames.forEach((info, frame) => {
if (info.mode === 'view' && info.cta.id === msg.ctaId && frame.contentWindow) frame.contentWindow.postMessage({ zm: 'live', s: msg.s }, '*');
});
// keep any other in-page copy of this CTA current (e.g. another tab's store)
const hit = Object.values(CTA_STORE).flatMap(o => Object.values(o).flat()).find(c => c.id === msg.ctaId);
if (hit) { hit.zLive = Object.assign(hit.zLive || {}, msg.s); }
});
window.addEventListener('message', e => {
let frame = null, info = null;
ZM.frames.forEach((v, f) => { if (f.contentWindow === e.source) { frame = f; info = v; } });
const m = e.data; if (!frame || !m || !m.zm) return;
const cta = info.cta;
if (m.zm === 'ready') frame.contentWindow.postMessage(zmInitPayload(cta, info.collIdx, info.mode), '*');
else if (m.zm === 'action' && info.mode === 'view') zmHandleAction(m, info);
else if (m.zm === 'event' && info.mode === 'view') zmLogEvent(m.type, { ctaId: cta.id, space: m.space && m.space.id });
else if (m.zm === 'dirty') { if (frame.id === 'zmFrame') ZM.dirty = !!m.v; }
else if (m.zm === 'live' && info.mode === 'edit') zmLiveTransport.publish({ ctaId: cta.id, s: m.s });
else if (m.zm === 'save' && info.mode === 'edit') {
cta.z = m.config; if (cta.zPage) cta.z.image = null;
cta.zLive = {}; // the saved config is the new baseline
zmRefreshChip(cta);
if (ctaSelectedId === cta.id) renderCtaContentTab(cta);
ZM.dirty = false; zmClose(true); toast('Zones saved ✓');
if (cta.z.mode === 'live') zmLiveTransport.publish({ ctaId: cta.id, s: {} });
}
});
/* ── Visitor viewer: show the map on the page it belongs to ── */
function zmInline(area, cta, collIdx) {
const wrap = document.createElement('div');
wrap.className = 'zm-inline-wrap';
const fr = document.createElement('iframe');
fr.className = 'zm-inline'; fr.title = cta.label || 'Interactive map';
wrap.appendChild(fr); area.appendChild(wrap);
zmRegister(fr, cta, collIdx, 'view');
fr.srcdoc = zmSrc();
}
function zmViewerHook(c) {
const collIdx = COLLS.indexOf(c); if (collIdx < 0) return;
const page = (typeof curPg === 'number' ? curPg : 0) + 1;
const ctas = ((CTA_STORE[collIdx] || {})[page] || []).filter(x => x.type === 'zones' && x.z && (x.z.regions || []).length);
if (!ctas.length) return;
const area = document.getElementById('contentArea'); if (!area) return;
// drop frames of the previous page
ZM.frames.forEach((v, f) => { if (f.classList.contains('zm-inline') && !document.body.contains(f)) ZM.frames.delete(f); });
ctas.forEach(cta => {
if (cta.zPage) { area.innerHTML = ''; zmInline(area, cta, collIdx); }
else {
const card = document.createElement('div');
card.className = 'zm-card';
card.innerHTML = '
' + zmThumbHTML(cta) + '
' + (cta.label || 'View map') + '';
card.querySelector('button').onclick = () => zmOpen(cta, collIdx, 'view');
area.appendChild(card);
}
});
}
/* ── Library shortcut: "Interactive map" ── */
function openMapUpload() {
openUploadModal(); zmIntent = true;
toast('Choose your floor plan or map (PNG, JPG or PDF). The spaces are found for you.');
}
function zmUpdateZonesCount(collIdx) {
COLLS[collIdx].zonesCount = Object.values(CTA_STORE[collIdx] || {}).flat().filter(c => c.type === 'zones' && c.z && (c.z.regions || []).length).length;
}
/* ── PDF → page images (pdf.js, loaded on demand) ── */
let _zmPdfLib = null;
function zmLoadPdfJs() {
if (_zmPdfLib) return _zmPdfLib;
const base = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/';
_zmPdfLib = new Promise((res, rej) => {
const sc = document.createElement('script'); sc.src = base + 'pdf.min.js';
sc.onload = () => {
fetch(base + 'pdf.worker.min.js').then(r => r.text()).then(t => {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([t], { type: 'text/javascript' }));
res(window.pdfjsLib);
}).catch(rej);
};
sc.onerror = () => rej(new Error('Could not load the PDF reader (no internet?)'));
document.head.appendChild(sc);
});
_zmPdfLib.catch(() => { _zmPdfLib = null; });
return _zmPdfLib;
}
async function zmRenderPdf(file, maxPages) {
const lib = await zmLoadPdfJs();
const pdf = await lib.getDocument({ data: await file.arrayBuffer() }).promise;
const n = Math.min(pdf.numPages, maxPages || 30), out = [];
for (let i = 1; i <= n; i++) {
const page = await pdf.getPage(i), vp0 = page.getViewport({ scale: 1 });
const vp = page.getViewport({ scale: Math.min(3, 1500 / vp0.width) });
const cv = document.createElement('canvas'); cv.width = Math.round(vp.width); cv.height = Math.round(vp.height);
const cx = cv.getContext('2d'); cx.fillStyle = '#fff'; cx.fillRect(0, 0, cv.width, cv.height);
await page.render({ canvasContext: cx, viewport: vp }).promise;
out.push(cv.toDataURL('image/jpeg', 0.85));
}
return { images: out, total: pdf.numPages };
}
function zmAttachPdf(newColl, file, mapIntent) {
toast('Reading the PDF pages…');
zmRenderPdf(file, 30).then(r => {
newColl.pageImgs = r.images; newColl.pages = r.images.length;
newColl.pdata = r.images.map((u, i) => ({ t: 'Page ' + (i + 1), s: '', b: '
' }));
applyCollFilters();
if (r.total > r.images.length) toast('Only the first ' + r.images.length + ' of ' + r.total + ' pages were read.');
if (mapIntent) {
openCtaEditor(COLLS.indexOf(newColl));
if (r.images.length === 1) setTimeout(() => dropCta('zones'), 350);
else toast('Go to the floor plan page, then click Zones.');
}
}).catch(e => toast('Could not read the PDF: ' + (e.message || e)));
}
/* ── Space actions: enquire / book / call back (visitor view) ── */
const ZM_REQUESTS = []; // call-back requests (in memory). Replace zmSubmitCallback() body with your API call.
const ZM_EVENTS = []; // visitor events (in memory): space_view, enquiry_whatsapp, enquiry_email, book_call, callback_request
function zmLogEvent(type, data) { ZM_EVENTS.push(Object.assign({ type, at: Date.now() }, data || {})); }
function zmRep() {
const pb = (typeof PB_RECORDS !== 'undefined' && PB_RECORDS[0]) || {}, lp = pb.landingPage || {};
return { name: lp.repName || 'Rahul Nair', email: lp.repEmail || 'rahul@effybiz.com', wa: lp.repWhatsapp || '919876543210' };
}
function zmHandleAction(m, info) {
const rep = zmRep(), sp = m.space || {}, map = m.mapTitle || (info.cta.label || 'the map');
const text = 'Hi ' + rep.name.split(' ')[0] + ', I\'m interested in ' + sp.id + ' (' + sp.status + ') on "' + map + '". Could you share more details?';
const ev = { ctaId: info.cta.id, space: sp.id };
if (m.kind === 'whatsapp') { zmLogEvent('enquiry_whatsapp', ev); openWhatsApp(rep.wa, text); }
else if (m.kind === 'email') { zmLogEvent('enquiry_email', ev); window.location.href = 'mailto:' + rep.email + '?subject=' + encodeURIComponent('Enquiry: ' + sp.id + ' – ' + map) + '&body=' + encodeURIComponent(text); }
else if (m.kind === 'book') { zmLogEvent('book_call', ev); openCalendar('book'); }
else if (m.kind === 'callback') zmOpenCallback(info, sp, map);
}
let zmCbCtx = null;
function zmOpenCallback(info, sp, map) {
zmCbCtx = { info, sp, map };
document.getElementById('zmCbFor').textContent = sp.id + ' · ' + sp.status + ' · ' + map;
document.getElementById('zmCbRep').textContent = zmRep().name;
document.getElementById('zmCbModal').classList.add('open');
setTimeout(() => document.getElementById('zmCbName').focus(), 50);
}
function zmCloseCallback() { document.getElementById('zmCbModal').classList.remove('open'); zmCbCtx = null; }
function zmSubmitCallback() {
if (!zmCbCtx) return;
const g = id => document.getElementById(id).value.trim();
const name = g('zmCbName'), phone = g('zmCbPhone');
if (!name) { toast('Please enter your name'); return; }
if (phone.replace(/\D/g, '').length < 8) { toast('Please enter a valid phone number'); return; }
const req = { id: 'cb' + Date.now(), name, phone, when: document.getElementById('zmCbWhen').value, note: g('zmCbNote'), space: zmCbCtx.sp.id, status: zmCbCtx.sp.status, map: zmCbCtx.map, ctaId: zmCbCtx.info.cta.id, to: zmRep().name, at: Date.now() };
ZM_REQUESTS.push(req); zmLogEvent('callback_request', { ctaId: req.ctaId, space: req.space });
zmCloseCallback();
['zmCbName', 'zmCbPhone', 'zmCbNote'].forEach(i => { document.getElementById(i).value = ''; });
toast('Request sent to ' + req.to + ' ✓ They will call you back.');
}
const CTA_META = {
button: { label: 'Button', ico: 'ti-cursor-text', color: '#C8973A', defaultText: 'Click Here →' },
shop: { label: 'Buy Now', ico: 'ti-shopping-cart', color: '#22C55E', defaultText: 'Add to Cart' },
video: { label: 'Video', ico: 'ti-player-play', color: '#EF4444', defaultText: '▶ Watch Now' },
audio: { label: 'Audio', ico: 'ti-music', color: '#8B5CF6', defaultText: '🎵 Listen' },
quiz: { label: 'Quiz', ico: 'ti-help-circle', color: '#3B82F6', defaultText: 'Take Quiz →' },
form: { label: 'Lead Form', ico: 'ti-forms', color: '#14B8A6', defaultText: 'Contact Us' },
calendar: { label: 'Book Meeting', ico: 'ti-calendar-event', color: '#F97316', defaultText: ' Book a Meeting' },
hotspot: { label: 'Hotspot', ico: 'ti-point', color: '#EC4899', defaultText: 'ℹ️' },
view360: { label: '360° View', ico: 'ti-rotate-360', color: '#0891B2', defaultText: '🔄 Spin Product' },
zones: { label: 'Zones (Map)', ico: 'ti-map-2', color: '#0EA5E9', defaultText: 'View map' },
};
const COLORS = ['#C8973A', '#22C55E', '#EF4444', '#3B82F6', '#8B5CF6', '#F97316', '#EC4899', '#0F1B2D', '#E11D48', '#0891B2'];
/* State */
let ctaEdCollIdx = 0, ctaEdPage = 1, ctaEdZoom = 1.0;
let ctaSelectedId = null;
let ctaDragging = false, ctaResizing = false;
let ctaDragOffX = 0, ctaDragOffY = 0;
let ctaResizeHandle = '', ctaResizeStartX = 0, ctaResizeStartY = 0;
let ctaResizeStartRect = null;
let cartItems = [];
let _audioDemoTimer = null, _audioPct = 0, _audioPlaying = false;
let ctaQuizData = null, ctaSelectedQuizOpt = null;
/* ── Open / close ── */
function openCtaEditor(collIdx) {
try {
ctaEdCollIdx = collIdx;
ctaEdPage = 1;
ctaEdZoom = 1.0;
ctaSelectedId = null;
if (!CTA_STORE[collIdx]) CTA_STORE[collIdx] = {};
const overlay = document.getElementById('ctaEdOverlay');
if (!overlay) { toast('CTA Editor overlay not found'); return; }
// Show overlay first so DOM is visible before we try to measure/render
overlay.classList.add('open');
const collNameEl = document.getElementById('ctaEdCollName');
if (collNameEl) collNameEl.textContent = '— ' + COLLS[collIdx].name;
const zoomEl = document.getElementById('ctaZoomLbl');
if (zoomEl) zoomEl.textContent = '100%';
// Render after a microtask so the overlay is painted
requestAnimationFrame(() => {
try {
renderCtaPage();
showCtaPanelEmpty();
} catch (e) {
console.error('CTA Editor render error:', e);
toast('CTA Editor error: ' + e.message);
}
});
} catch (e) {
console.error('openCtaEditor error:', e);
toast('Could not open CTA Editor: ' + e.message);
}
}
function closeCtaEditor() {
document.getElementById('ctaEdOverlay').classList.remove('open');
ctaSelectedId = null;
}
function saveCtaEditor() {
const total = Object.values(CTA_STORE[ctaEdCollIdx] || {}).reduce((a, v) => a + v.length, 0);
COLLS[ctaEdCollIdx].ctaCount = total;
zmUpdateZonesCount(ctaEdCollIdx);
const dot = document.getElementById('ctaDot' + ctaEdCollIdx);
if (dot) dot.classList.toggle('show', total > 0);
toast(total + ' CTA' + (total !== 1 ? 's' : '') + ' saved ✓');
closeCtaEditor();
try { applyCollFilters(); } catch (e) {}
}
function updEdCount() {
const total = Object.values(CTA_STORE[ctaEdCollIdx] || {}).reduce((a, v) => a + v.length, 0);
const el = document.getElementById('ctaEdCount');
if (el) el.textContent = total + ' CTA' + (total !== 1 ? 's' : '');
}
function updDropHint() {
const total = Object.values(CTA_STORE[ctaEdCollIdx] || {}).reduce((a, v) => a + v.length, 0);
const hint = document.getElementById('ctaDropHint');
if (hint) hint.classList.toggle('hidden', total > 0);
}
/* ── Page render ── */
function renderCtaPage() {
const page = document.getElementById('ctaPage');
page.innerHTML = getCtaPageContent(ctaEdCollIdx, ctaEdPage);
page.style.transform = 'scale(' + ctaEdZoom + ')';
const c = COLLS[ctaEdCollIdx];
const maxPg = c.pages || 5;
(document.getElementById('ctaPgLabel')||document.createElement('div')).textContent = 'P ' + ctaEdPage + ' / ' + maxPg;
document.getElementById('ctaPgPrev').disabled = ctaEdPage === 1;
document.getElementById('ctaPgNext').disabled = ctaEdPage >= maxPg;
// render existing chips for this page
const chips = (CTA_STORE[ctaEdCollIdx][ctaEdPage] || []);
chips.forEach(cta => appendChipEl(cta));
updDropHint();
}
function getCtaPageContent(collIdx, pgNum) {
const c = COLLS[collIdx];
{ const _pi = zmPageImage(collIdx, pgNum); if (_pi) return '
'; }
const isDeck = c && (c.type === 'ppt' || c.name.toLowerCase().includes('.pptx'));
const type = isDeck ? 'deck' : 'pdf';
const pages = PAGE_CONTENT[type];
const key = Math.min(pgNum, Object.keys(pages).length);
return (pages[key] || pages[1]) + '';
}
function ctaChangePg(dir) {
const maxPg = (COLLS[ctaEdCollIdx].pages || 5);
ctaEdPage = Math.max(1, Math.min(maxPg, ctaEdPage + dir));
ctaSelectedId = null;
showCtaPanelEmpty();
renderCtaPage();
}
function ctaZoom(delta) {
ctaEdZoom = Math.max(0.4, Math.min(1.6, ctaEdZoom + delta));
(document.getElementById('ctaZoomLbl')||document.createElement('div')).textContent = Math.round(ctaEdZoom * 100) + '%';
(document.getElementById('ctaPage')||{}).style.transform = 'scale(' + ctaEdZoom + ')';
}
/* ── Drop a new CTA onto the page ── */
function dropCta(type) {
const meta = CTA_META[type];
const id = Date.now();
const cta = {
id, type,
label: meta.defaultText,
color: meta.color,
borderRadius: '8px',
size: 'md',
iconCls: '',
anim: 'none',
animSpeed: 'normal',
// position: center of visible canvas, as px on the 700px-wide page
x: 220, y: 140, w: 200, h: 44,
// type-specific fields
url: '', title: '', productName: '', price: '', question: '',
options: ['Option A', 'Option B', 'Option C'], correct: '',
tooltip: '', formTitle: 'Get in Touch',
// 360° view fields
view360Mode: 'embed', // 'embed' | 'frames'
view360Url: '', // Sketchfab/model-viewer URL
view360Frames: [], // uploaded frame dataURLs (base64)
view360FrameCount: 0,
view360AutoSpin: false,
view360Sensitivity: 3,
view360ProductName: '',
};
if (type === 'zones') {
cta.z = null; cta.zPage = !!zmPageImage(ctaEdCollIdx, ctaEdPage); cta.zPageNo = ctaEdPage;
if (cta.zPage) { cta.x = 0; cta.y = 0; cta.w = 700; cta.h = 500; } else { cta.x = 210; cta.y = 120; cta.w = 280; cta.h = 180; }
}
if (!CTA_STORE[ctaEdCollIdx][ctaEdPage]) CTA_STORE[ctaEdCollIdx][ctaEdPage] = [];
CTA_STORE[ctaEdCollIdx][ctaEdPage].push(cta);
appendChipEl(cta);
selectCta(id);
updEdCount();
updDropHint();
if (type === 'zones') zmAfterDrop(cta);
}
/* ── Build chip DOM element ── */
function appendChipEl(cta) {
const page = document.getElementById('ctaPage');
const el = document.createElement('div');
el.className = 'ctaed-chip' + animClass(cta);
el.id = 'ctaChip' + cta.id;
el.style.cssText = 'left:' + cta.x + 'px;top:' + cta.y + 'px;width:' + cta.w + 'px;height:' + cta.h + 'px;pointer-events:all';
el.innerHTML = chipInnerHTML(cta) +
'
' +
['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'].map(h =>
'
'
).join('');
el.addEventListener('mousedown', onChipMousedown);
el.querySelectorAll('.ctaed-handle').forEach(h => h.addEventListener('mousedown', onHandleMousedown));
page.appendChild(el);
}
function chipInnerHTML(cta) {
if (cta.type === 'zones') return '
' + zmThumbHTML(cta) + '
';
// Font size by size setting — no vertical padding (chip height controls that)
const sizeMap = { sm: 'font-size:11px', md: 'font-size:13px', lg: 'font-size:15px' };
const sz = sizeMap[cta.size] || sizeMap.md;
const baseStyle = 'background:' + cta.color + ';color:white;border-radius:' + cta.borderRadius + ';' + sz + ';width:100%;height:100%;box-sizing:border-box;padding:0 14px;';
if (cta.type === 'view360') {
return '
' +
'
' + (cta.label || '🔄 Spin Product') + '
' +
(cta.view360Mode === 'frames' && cta.view360FrameCount > 0
? '
' + cta.view360FrameCount + ' frames · drag to spin
'
: cta.view360Url ? '
3D Embed
' : '
Configure in panel →
'
) +
'
';
}
const icon = cta.iconCls ? '
' : '';
return '
' + icon + '' + (cta.label || '') + '
';
}
function animClass(cta) {
let cls = cta.anim && cta.anim !== 'none' ? ' anim-' + cta.anim : '';
if (cta.animSpeed && cta.animSpeed !== 'normal') cls += ' anim-speed-' + cta.animSpeed;
return cls;
}
function refreshChipEl(cta) {
if (cta.type === 'zones') { zmRefreshChip(cta); return; }
const el = document.getElementById('ctaChip' + cta.id);
if (!el) return;
el.className = 'ctaed-chip' + animClass(cta) + (ctaSelectedId === cta.id ? ' selected' : '');
el.style.left = cta.x + 'px';
el.style.top = cta.y + 'px';
el.style.width = cta.w + 'px';
el.style.height = cta.h + 'px';
const sizeMap = { sm: 'font-size:11px', md: 'font-size:13px', lg: 'font-size:15px' };
const sz = sizeMap[cta.size] || sizeMap.md;
const icon = cta.iconCls ? '
' : '';
const inner = el.querySelector('.ctaed-chip-inner');
if (inner) {
inner.style.cssText = 'background:' + cta.color + ';color:white;border-radius:' + cta.borderRadius + ';' + sz + ';width:100%;height:100%;box-sizing:border-box;padding:0 14px;display:flex;align-items:center;justify-content:center;gap:7px';
inner.innerHTML = icon + '
' + (cta.label || '') + ' ';
}
}
/* ── Selection ── */
function selectCta(id) {
ctaSelectedId = id;
document.querySelectorAll('.ctaed-chip').forEach(el => el.classList.remove('selected'));
const el = document.getElementById('ctaChip' + id);
if (el) el.classList.add('selected');
const cta = findCta(id);
if (!cta) return;
showCtaPanelActive(cta);
}
function ctaDeselectAll(e) {
if (e.target === document.getElementById('ctaPageWrap') || e.target === document.getElementById('ctaPage')) {
ctaSelectedId = null;
document.querySelectorAll('.ctaed-chip').forEach(el => el.classList.remove('selected'));
showCtaPanelEmpty();
}
}
function findCta(id) {
const pg = CTA_STORE[ctaEdCollIdx][ctaEdPage] || [];
return pg.find(c => c.id === id);
}
/* ── Drag (move) ── */
function onChipMousedown(e) {
if (e.target.closest('.ctaed-handle') || e.target.closest('.ctaed-chip-del')) return;
e.preventDefault(); e.stopPropagation();
const el = e.currentTarget;
const id = parseInt(el.id.replace('ctaChip', ''));
selectCta(id);
const { ox, oy } = ctaPageOrigin();
ctaDragOffX = (e.clientX - ox) - parseFloat(el.style.left) * ctaEdZoom;
ctaDragOffY = (e.clientY - oy) - parseFloat(el.style.top) * ctaEdZoom;
ctaDragging = true;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
}
function onDragMove(e) {
if (!ctaDragging || !ctaSelectedId) return;
const { ox, oy } = ctaPageOrigin();
const cta = findCta(ctaSelectedId);
if (!cta) return;
cta.x = Math.max(0, ((e.clientX - ox) - ctaDragOffX) / ctaEdZoom);
cta.y = Math.max(0, ((e.clientY - oy) - ctaDragOffY) / ctaEdZoom);
const el = document.getElementById('ctaChip' + ctaSelectedId);
if (el) { el.style.left = cta.x + 'px'; el.style.top = cta.y + 'px'; }
}
function onDragUp() {
ctaDragging = false;
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
}
/* ── Resize ── */
function onHandleMousedown(e) {
e.preventDefault(); e.stopPropagation();
const chip = e.currentTarget.closest('.ctaed-chip');
const id = parseInt(chip.id.replace('ctaChip', ''));
selectCta(id);
const cta = findCta(id);
if (!cta) return;
ctaResizing = true;
ctaResizeHandle = e.currentTarget.dataset.handle;
ctaResizeStartX = e.clientX;
ctaResizeStartY = e.clientY;
ctaResizeStartRect = { x: cta.x, y: cta.y, w: cta.w, h: cta.h };
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeUp);
}
function onResizeMove(e) {
if (!ctaResizing || !ctaSelectedId) return;
const dx = (e.clientX - ctaResizeStartX) / ctaEdZoom;
const dy = (e.clientY - ctaResizeStartY) / ctaEdZoom;
const r = ctaResizeStartRect;
const cta = findCta(ctaSelectedId);
if (!cta) return;
const MIN_W = 60; // minimum width
const MIN_H = 24; // minimum height — allow very compact buttons
const h = ctaResizeHandle;
let nx = r.x, ny = r.y, nw = r.w, nh = r.h;
if (h.includes('e')) nw = Math.max(MIN_W, r.w + dx);
if (h.includes('s')) nh = Math.max(MIN_H, r.h + dy);
if (h.includes('w')) { nw = Math.max(MIN_W, r.w - dx); nx = r.x + r.w - nw; }
if (h.includes('n')) { nh = Math.max(MIN_H, r.h - dy); ny = r.y + r.h - nh; }
cta.x = nx; cta.y = ny; cta.w = nw; cta.h = nh;
const el = document.getElementById('ctaChip' + ctaSelectedId);
if (el) { el.style.left = nx + 'px'; el.style.top = ny + 'px'; el.style.width = nw + 'px'; el.style.height = nh + 'px'; }
}
function onResizeUp() {
ctaResizing = false;
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeUp);
}
/* Get the top-left corner of the page in screen coords (accounts for scroll in wrap) */
function ctaPageOrigin() {
const wrap = document.getElementById('ctaPageWrap');
const page = document.getElementById('ctaPage');
const wRect = wrap.getBoundingClientRect();
// The page is centered in the wrap. Its natural width is 700px.
// With scale(zoom) the visual size is 700*zoom but layout size is still 700px.
// The page's offsetLeft/Top inside the wrap gives the unscaled position.
const pageNaturalW = page.offsetWidth;
const pageNaturalH = page.offsetHeight;
// Visual size after scale
const visW = pageNaturalW * ctaEdZoom;
const visH = pageNaturalH * ctaEdZoom;
// Page is centered in wrap (transform-origin: top center)
const ox = wRect.left + (wRect.width - visW) / 2;
const oy = wRect.top + wrap.scrollTop * 0 - wrap.scrollTop + (wRect.height > visH ? (wRect.height - visH) / 2 : 0);
// Simpler: just use getBoundingClientRect on the page itself — it gives scaled visual rect
const pRect = page.getBoundingClientRect();
return { ox: pRect.left, oy: pRect.top };
}
/* ── Delete ── */
function deleteSelectedCta(id) {
const tid = id || ctaSelectedId;
if (!tid) return;
const pg = CTA_STORE[ctaEdCollIdx][ctaEdPage];
if (pg) { const i = pg.findIndex(c => c.id === tid); if (i > -1) pg.splice(i, 1); }
const el = document.getElementById('ctaChip' + tid);
if (el) el.remove();
ctaSelectedId = null;
showCtaPanelEmpty();
updEdCount();
updDropHint();
}
/* ── Panel: empty state ── */
function showCtaPanelEmpty() {
document.getElementById('ctaPanelEmpty').classList.remove('hidden');
document.getElementById('ctaPanelActive').classList.add('hidden');
// render mini list
const all = Object.entries(CTA_STORE[ctaEdCollIdx] || {}).flatMap(([pg, ctAs]) => ctAs.map(c => ({ ...c, pg: Number(pg) })));
const mini = document.getElementById('ctaPlacedMini');
if (!all.length) { mini.innerHTML = '
No CTAs placed yet
'; return; }
mini.innerHTML = '
All CTAs
' +
all.map(c => {
const m = CTA_META[c.type] || CTA_META.button;
return '
' +
'
' +
'
' + (c.label || m.label) + '
' +
'
P' + c.pg + '
' +
'
';
}).join('');
}
/* ── Panel: active config ── */
function showCtaPanelActive(cta) {
document.getElementById('ctaPanelEmpty').classList.add('hidden');
document.getElementById('ctaPanelActive').classList.remove('hidden');
const m = CTA_META[cta.type] || CTA_META.button;
// header
(document.getElementById('ctaPanelIco')||{}).style.cssText = 'background:' + cta.color + '22;color:' + cta.color + ';width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0';
(document.getElementById('ctaPanelIco')||document.createElement('div')).innerHTML = '
';
(document.getElementById('ctaPanelType')||document.createElement('div')).textContent = m.label;
// reset to Content tab
switchCtaTab('content', document.querySelector('.ctaed-tab'));
document.querySelectorAll('.ctaed-tab').forEach((t, i) => { t.style.display = (cta.type === 'zones' && i > 0) ? 'none' : ''; });
renderCtaContentTab(cta);
renderCtaStyleTab(cta);
renderCtaAnimTab(cta);
}
function switchCtaTab(tab, el) {
document.querySelectorAll('.ctaed-tab').forEach(t => t.classList.remove('active'));
if (el) el.classList.add('active');
document.getElementById('ctaTabContent').classList.toggle('hidden', tab !== 'content');
document.getElementById('ctaTabStyle').classList.toggle('hidden', tab !== 'style');
document.getElementById('ctaTabAnimation').classList.toggle('hidden', tab !== 'animation');
}
function renderCtaAnimTab(cta) {
// Mark active animation chip
document.querySelectorAll('.ctaed-anim-chip-big').forEach(b => {
const span = b.querySelector('span');
if (!span) return;
const txt = span.textContent.toLowerCase().replace(/[^a-z]/g, '');
b.classList.toggle('active', txt === (cta.anim || 'none'));
});
// Mark active speed chip
document.querySelectorAll('#ctaTabAnimation .ctaed-anim-chip').forEach(b => {
b.classList.toggle('active', b.textContent.toLowerCase() === (cta.animSpeed || 'normal'));
});
}
/* ── Content tab ── */
function renderCtaContentTab(cta) {
const f = document.getElementById('ctaContentFields');
let html = '';
// Inputs that auto-preview on every keystroke
const v = (id, val, ph, field) => '
';
const tv = (id, val, ph, field) => '
';
const lbl = text => '
' + text + ' ';
if (cta.type === 'zones') {
html = zmPanelHTML(cta, lbl, v);
} else if (cta.type === 'button') {
const isPartner = cta.linkType === 'partner';
html =
lbl('Button Label') + v('cfLabel', cta.label, 'Click Here →', 'label') +
lbl('Link Type') +
'
' +
' URL ' +
' Partner Link ' +
'
' +
'
' +
lbl('URL') + v('cfUrl', cta.url, 'https://...', 'url') +
lbl('Open In') + 'New tab Same tab ' +
'
' +
'
' +
'
' +
'
Partner Link
' +
'
The partner link will be automatically generated from the backend and injected when this collateral is shared. No URL input is needed.
' +
'
Unique per recipient · tracked · auto-expires
' +
'
' +
'
';
} else if (cta.type === 'shop') {
html = lbl('Button Label') + v('cfLabel', cta.label, 'Add to Cart', 'label') + lbl('Product Name') + v('cfProdName', cta.productName, 'e.g. Growth Plan', 'productName') +
lbl('Price') + v('cfPrice', cta.price, '₹14,999', 'price') + lbl('Checkout URL') + v('cfUrl', cta.url, 'https://...', 'url');
} else if (cta.type === 'video') {
const isUpload = cta.videoMode === 'upload';
const isRecord = cta.videoMode === 'record';
html =
lbl('Button Label') + v('cfLabel', cta.label, '▶ Watch Now', 'label') +
lbl('Video Title') + v('cfTitle', cta.title, 'Product Walkthrough', 'title') +
lbl('Source') +
'
' +
' URL ' +
' Upload ' +
' Record ' +
'
' +
'
' +
lbl('YouTube / Embed URL') + v('cfUrl', cta.url, 'https://youtube.com/embed/...', 'url') +
'
' +
'
' +
'
';
} else if (cta.type === 'audio') {
const isUpload = cta.audioMode === 'upload';
const isRecord = cta.audioMode === 'record';
html =
lbl('Button Label') + v('cfLabel', cta.label, '🎵 Listen', 'label') +
lbl('Audio Title') + v('cfTitle', cta.title, 'CEO Message', 'title') +
lbl('Source') +
'
' +
' Upload ' +
' Record ' +
'
' +
'
' +
'
';
} else if (cta.type === 'quiz') {
const questions = cta.questions || [{ text: '', options: ['Option A', 'Option B', 'Option C'], correctIdx: 0, explanation: '' }];
const qs = cta.quizSettings || {};
html =
lbl('Quiz Title') + v('cfLabel', cta.label, 'Take Quiz →', 'label') +
/* Sub-tabs: Questions | Settings */
'
' +
'Questions ' +
'⚙ Settings ' +
'
' +
/* Questions pane */
'
' +
'
' + questions.map((q, qi) => buildQCard(q, qi)).join('') + '
' +
'
Add Question' +
'
' +
/* Settings pane (hidden by default) */
'
' +
'
' +
'
⭐ Show Score
' +
'
🏆 Leaderboard
' +
'
🎓 Certificate
' +
'
🔊 Sound FX
' +
'
' +
'
' +
'
Pass Score Threshold
' +
'
' +
' ' +
'' + (qs.passScore || 70) + '% ' +
'
' +
'
Minimum score to show certificate and pass badge
' +
'
' +
'
' +
'
Points per correct answer
' +
'
' +
'
' +
'
';
} else if (cta.type === 'form') {
html = lbl('Button Label') + v('cfLabel', cta.label, 'Contact Us', 'label') + lbl('Form Title') + v('cfFormTitle', cta.formTitle, 'Get in Touch', 'formTitle');
} else if (cta.type === 'calendar') {
html = lbl('Button Label') + v('cfLabel', cta.label, ' Book a Meeting', 'label') + lbl('Calendly URL') + v('cfUrl', cta.url, 'https://calendly.com/...', 'url');
} else if (cta.type === 'hotspot') {
html = lbl('Trigger Label') + v('cfLabel', cta.label, 'ℹ️', 'label') + lbl('Tooltip Text') + tv('cfTooltip', cta.tooltip, 'This feature allows you to...', 'tooltip');
} else if (cta.type === 'view360') {
const isEmbed = (cta.view360Mode || 'embed') === 'embed';
html =
lbl('Button Label') + v('cfLabel', cta.label, '🔄 Spin Product', 'label') +
lbl('Product Name') + v('cf360Name', cta.view360ProductName, 'e.g. FinScan Pro Device') +
lbl('Source Mode') +
'
' +
' 3D Embed ' +
' Photo Frames ' +
'
' +
/* ── 3D Embed pane ── */
'
' +
lbl('Sketchfab / Model-viewer URL') +
v('cf360Url', cta.view360Url, 'https://sketchfab.com/models/…/embed') +
'
' +
'
How to get an embed URL
' +
'
' +
'Sketchfab — upload your 3D model at sketchfab.com → open the model → click Embed → copy the iframe src URL ' +
'GLB/GLTF file — host your .glb file on any CDN or Google Drive (public link) and paste the direct URL — model-viewer handles it automatically ' +
'Other viewers — any iframe-compatible 3D viewer URL works (three.js, p3d.in, etc.) ' +
' ' +
'
' +
'
' +
/* ── Photo Frames pane ── */
'
' +
/* Upload zone */
lbl('Upload Frames') +
'
' +
'
' +
'
Click to select your product photosPNG · JPG · WebP · 24–36 images recommended
' +
'
' +
'
' +
'
' +
/* How to photograph guide */
'
' +
'
How to photograph your product for 360°
' +
'
' +
'Set up a turntable — place the product on a rotating platform (a lazy susan works perfectly). Keep the camera fixed on a tripod. ' +
'Rotate in equal steps — for 36 frames, rotate the product 10° per shot . For 24 frames, rotate 15° per shot. Use a protractor or turntable markings. ' +
'Consistent lighting — use diffused softbox lighting or a lightbox tent. Avoid shadows shifting between shots. ' +
'Name files in order — rename photos as 001.jpg, 002.jpg … 036.jpg before uploading — the order determines the spin direction. ' +
'White/neutral background — a clean background makes the product stand out and keeps file sizes small. ' +
' ' +
'
Tip: Use a 10–15 MP phone camera in portrait mode with HDR off for clean, consistent results.
' +
'
' +
/* File naming tip */
'
' +
'
File naming — must be in clockwise order
' +
'
' +
['001', '002', '003', '…', '036'].map(n => '' + n + '.jpg').join(' ') +
'
' +
'
Frames are sorted alphabetically on upload — correct naming ensures smooth rotation.
' +
'
' +
'
' + /* end FramesPane */
/* ── Settings (both modes) ── */
lbl('Settings') +
'
';
}
f.innerHTML = html;
}
function buildQCard(q, qi) {
const opts = (q.options || []).map((o, oi) =>
'
'
).join('');
return '
' +
'' +
'
' +
'
' + opts + '
' +
'
Add option' +
'
' +
'
';
}
function addQuizQuestion() {
const cta = findCta(ctaSelectedId); if (!cta) return;
if (!cta.questions) cta.questions = [{ text: '', options: ['Option A', 'Option B'], correctIdx: 0, explanation: '' }];
cta.questions.push({ text: '', options: ['Option A', 'Option B'], correctIdx: 0, explanation: '' });
const container = document.getElementById('qbContainer');
if (container) {
const qi = cta.questions.length - 1;
container.insertAdjacentHTML('beforeend', buildQCard(cta.questions[qi], qi));
}
}
function rmQBQuestion(qi) {
const cta = findCta(ctaSelectedId); if (!cta || !cta.questions) return;
cta.questions.splice(qi, 1);
renderCtaContentTab(cta);
}
function addQBOpt(qi) {
const cta = findCta(ctaSelectedId); if (!cta) return;
if (!cta.questions) return;
cta.questions[qi].options.push('Option ' + String.fromCharCode(65 + cta.questions[qi].options.length));
const container = document.getElementById('qbOpts' + qi);
if (container) {
const oi = cta.questions[qi].options.length - 1;
container.insertAdjacentHTML('beforeend',
'
'
);
}
}
function rmQBOpt(qi, oi) {
const cta = findCta(ctaSelectedId); if (!cta || !cta.questions) return;
if (cta.questions[qi].options.length <= 2) return;
cta.questions[qi].options.splice(oi, 1);
if (cta.questions[qi].correctIdx >= cta.questions[qi].options.length) cta.questions[qi].correctIdx = 0;
const container = document.getElementById('qbOpts' + qi);
if (container) container.innerHTML = cta.questions[qi].options.map((o, i) =>
'
'
).join('');
}
function toggleCorrect(qi, oi) {
const cta = findCta(ctaSelectedId); if (!cta || !cta.questions) return;
cta.questions[qi].correctIdx = oi;
document.querySelectorAll('#qbCard' + qi + ' .qb-correct-dot').forEach((d, i) => d.classList.toggle('correct', i === oi));
}
function toggleQSetting(el, key) {
el.classList.toggle('on');
const cta = findCta(ctaSelectedId); if (!cta) return;
if (!cta.quizSettings) cta.quizSettings = {};
cta.quizSettings[key] = el.classList.contains('on');
}
function toggleQSetting2(key, val) {
const cta = findCta(ctaSelectedId); if (!cta) return;
if (!cta.quizSettings) cta.quizSettings = {};
cta.quizSettings[key] = val;
}
function switchQuizSubTab(tab, el) {
document.querySelectorAll('.quiz-subtab').forEach(b => {
b.style.color = 'rgba(255,255,255,.4)';
b.style.borderBottomColor = 'transparent';
});
el.style.color = 'var(--gold2)';
el.style.borderBottomColor = 'var(--gold)';
const qPane = document.getElementById('qzQPane');
const sPane = document.getElementById('qzSPane');
if (qPane) qPane.style.display = tab === 'questions' ? '' : 'none';
if (sPane) sPane.style.display = tab === 'settings' ? '' : 'none';
}
/* ── Apply: read quiz builder fields ── */
function applyCtaConfig() {
const cta = findCta(ctaSelectedId);
if (!cta) return;
const g = id => document.getElementById(id);
if (g('cfLabel')) cta.label = g('cfLabel').value;
if (g('cfUrl') && cta.linkType !== 'partner') cta.url = g('cfUrl').value;
// save link type for button
if (cta.type === 'button') {
const partnerActive = document.getElementById('btnLinkPartner');
if (partnerActive && partnerActive.classList.contains('active')) cta.linkType = 'partner';
else cta.linkType = 'url';
}
if (g('cfTitle')) cta.title = g('cfTitle').value;
if (g('cfProdName')) cta.productName = g('cfProdName').value;
if (g('cfPrice')) cta.price = g('cfPrice').value;
if (g('cfFormTitle')) cta.formTitle = g('cfFormTitle').value;
if (g('cfTooltip')) cta.tooltip = g('cfTooltip').value;
// video/audio mode
if (cta.type === 'video') {
if (document.getElementById('cfUrl')) cta.url = document.getElementById('cfUrl').value;
if (document.getElementById('cfTitle')) cta.title = document.getElementById('cfTitle').value;
}
if (cta.type === 'audio') {
if (document.getElementById('cfTitle')) cta.title = document.getElementById('cfTitle').value;
}
// 360° view fields
if (cta.type === 'view360') {
if (g('cf360Name')) cta.view360ProductName = g('cf360Name').value;
if (g('cf360Url')) cta.view360Url = g('cf360Url').value;
if (g('cf360AutoSpin')) cta.view360AutoSpin = g('cf360AutoSpin').checked;
if (g('cf360Sensitivity')) cta.view360Sensitivity = parseInt(g('cf360Sensitivity').value);
}
// Quiz multi-question
if (cta.type === 'quiz') {
const qtexts = document.querySelectorAll('[data-qtext]');
const qexpl = document.querySelectorAll('[data-qexpl]');
if (qtexts.length) {
if (!cta.questions) cta.questions = [];
qtexts.forEach((el, qi) => {
if (!cta.questions[qi]) cta.questions[qi] = { text: '', options: [], correctIdx: 0, explanation: '' };
cta.questions[qi].text = el.value;
});
qexpl.forEach((el, qi) => { if (cta.questions[qi]) cta.questions[qi].explanation = el.value; });
document.querySelectorAll('[data-qi]').forEach(el => {
if (!el.dataset.oi) return;
const qi = parseInt(el.dataset.qi), oi = parseInt(el.dataset.oi);
if (cta.questions[qi] && cta.questions[qi].options) cta.questions[qi].options[oi] = el.value;
});
}
}
refreshChipEl(cta);
toast('Updated ✓');
}
/* ── 360° admin helpers ── */
function set360Mode(mode, btn) {
const cta = findCta(ctaSelectedId); if (!cta) return;
cta.view360Mode = mode;
(document.getElementById('v360EmbedPane')||{}).style.display = mode === 'embed' ? '' : 'none';
(document.getElementById('v360FramesPane')||{}).style.display = mode === 'frames' ? '' : 'none';
document.querySelectorAll('#v360ModeEmbed,#v360ModeFrames').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
function v360LoadFrames(input) {
const cta = findCta(ctaSelectedId); if (!cta) return;
const files = Array.from(input.files).sort((a, b) => a.name.localeCompare(b.name));
if (!files.length) return;
cta.view360Frames = [];
cta.view360FrameCount = files.length;
const strip = document.getElementById('v360FrameStrip');
const lbl = document.getElementById('v360UploadLabel');
let loaded = 0;
strip.innerHTML = '';
files.forEach((f, i) => {
const reader = new FileReader();
reader.onload = e => {
cta.view360Frames[i] = e.target.result;
loaded++;
// Show thumbnail for first few frames
if (i < 8) {
const img = document.createElement('img');
img.src = e.target.result;
img.style.cssText = 'width:36px;height:28px;object-fit:cover;border-radius:4px;border:1px solid rgba(255,255,255,.15)';
strip.appendChild(img);
}
if (loaded === files.length) {
lbl.innerHTML = '
' + files.length + ' frames loaded · drag left/right to spin';
toast(files.length + ' frames loaded ✓');
}
};
reader.readAsDataURL(f);
});
}
/* ── Media source mode (video/audio URL | Upload | Record) ── */
function setMediaMode(mediaType, mode, btn) {
const cta = findCta(ctaSelectedId); if (!cta) return;
if (mediaType === 'video') {
cta.videoMode = mode;
btn.closest('.ctaed-anim-row, div').querySelectorAll('.ctaed-anim-chip').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
(document.getElementById('cfVideoUrlPane')||{}).style.display = mode === 'url' ? '' : 'none';
(document.getElementById('cfVideoUploadPane')||{}).style.display = mode === 'upload' ? '' : 'none';
(document.getElementById('cfVideoRecordPane')||{}).style.display = mode === 'record' ? '' : 'none';
} else {
cta.audioMode = mode;
btn.closest('div').querySelectorAll('.ctaed-anim-chip').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
(document.getElementById('cfAudioUploadPane')||{}).style.display = mode === 'upload' ? '' : 'none';
(document.getElementById('cfAudioRecordPane')||{}).style.display = mode === 'record' ? '' : 'none';
}
}
function mediaFileChosen(type, input) {
const file = input.files[0]; if (!file) return;
const cta = findCta(ctaSelectedId); if (!cta) return;
const reader = new FileReader();
reader.onload = e => {
if (type === 'video') {
cta.videoDataUrl = e.target.result;
const lbl = document.getElementById('cfVideoUploadLbl');
if (lbl) lbl.innerHTML = '
' + file.name + ' (' + (file.size / 1048576).toFixed(1) + ' MB)';
} else {
cta.audioDataUrl = e.target.result;
const lbl = document.getElementById('cfAudioUploadLbl');
if (lbl) lbl.innerHTML = '
' + file.name;
}
toast(file.name + ' loaded');
};
reader.readAsDataURL(file);
}
/* CTA editor recording state (separate from wizard message recording) */
const CTA_MREC = { stream: null, recorder: null, chunks: [], type: null, timer: null, secs: 0 };
async function mediaStartRecord(type) {
try {
const constraints = type === 'video' ? { video: true, audio: true } : { audio: true };
CTA_MREC.stream = await navigator.mediaDevices.getUserMedia(constraints);
CTA_MREC.type = type; CTA_MREC.chunks = []; CTA_MREC.secs = 0;
CTA_MREC.recorder = new MediaRecorder(CTA_MREC.stream);
CTA_MREC.recorder.ondataavailable = e => { if (e.data.size) CTA_MREC.chunks.push(e.data); };
CTA_MREC.recorder.onstop = () => {
const blob = new Blob(CTA_MREC.chunks, { type: type === 'video' ? 'video/webm' : 'audio/webm' });
const url = URL.createObjectURL(blob);
const cta = findCta(ctaSelectedId);
if (cta) {
if (type === 'video') { cta.videoDataUrl = url; cta.videoBlob = blob; }
else { cta.audioDataUrl = url; cta.audioBlob = blob; }
}
if (type === 'video') {
const pv = document.getElementById('cfVideoPreview');
if (pv) { pv.src = url; pv.style.display = 'block'; }
} else {
const pb = document.getElementById('cfAudioPlayback');
if (pb) { pb.src = url; pb.style.display = 'block'; }
}
CTA_MREC.stream.getTracks().forEach(t => t.stop());
clearInterval(CTA_MREC.timer);
document.getElementById('cf' + (type === 'video' ? 'Video' : 'Audio') + 'RecordUI').style.display = '';
document.getElementById('cf' + (type === 'video' ? 'Video' : 'Audio') + 'RecStatus').style.display = 'none';
toast('Recording saved ✓');
};
CTA_MREC.recorder.start();
if (type === 'video') {
const pv = document.getElementById('cfVideoPreview');
if (pv) { pv.srcObject = CTA_MREC.stream; pv.style.display = 'block'; pv.muted = true; pv.play(); }
}
document.getElementById('cf' + (type === 'video' ? 'Video' : 'Audio') + 'RecordUI').style.display = 'none';
document.getElementById('cf' + (type === 'video' ? 'Video' : 'Audio') + 'RecStatus').style.display = '';
CTA_MREC.timer = setInterval(() => {
CTA_MREC.secs++;
const t = document.getElementById('cf' + (type === 'video' ? 'Video' : 'Audio') + 'RecTime');
if (t) t.textContent = Math.floor(CTA_MREC.secs / 60) + ':' + String(CTA_MREC.secs % 60).padStart(2, '0');
}, 1000);
} catch (err) {
toast('Microphone/camera permission denied: ' + err.message);
}
}
function mediaStopRecord(type) {
if (CTA_MREC.recorder && CTA_MREC.recorder.state !== 'inactive') CTA_MREC.recorder.stop();
}
/* ══════════════════════════════════════
COLOR SYSTEM
══════════════════════════════════════ */
const BASE_COLORS = ['#C8973A', '#22C55E', '#EF4444', '#3B82F6', '#8B5CF6', '#F97316', '#EC4899', '#0F1B2D', '#E11D48', '#0891B2', '#65A30D', '#D97706', '#6366F1', '#14B8A6'];
let savedColors = JSON.parse(localStorage.getItem('ctaSavedColors') || '[]');
let savedStyles = JSON.parse(localStorage.getItem('ctaSavedStyles') || '[]');
let gradStops = [{ color: '#C8973A', pct: 0 }, { color: '#F97316', pct: 100 }];
let gradDir = 'to right';
let currentHexPreview = '#C8973A';
function colorSysTab(tab, el) {
document.querySelectorAll('.color-sys-tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
document.querySelectorAll('.color-pane').forEach(p => p.classList.remove('active'));
document.getElementById('colorPane' + tab.charAt(0).toUpperCase() + tab.slice(1)).classList.add('active');
if (tab === 'saved') renderSavedPalette();
if (tab === 'gradient') renderGradStops();
}
function renderCtaColorSwatches() {
const swatches = BASE_COLORS.map(c =>
'
'
).join('') + '
';
const el = document.getElementById('ctaColorSwatches');
if (el) el.innerHTML = swatches;
}
function pickColorHex(hex) {
currentHexPreview = hex;
const inp = document.getElementById('colorHexInput');
const dot = document.getElementById('colorPreviewDot');
if (inp) inp.value = hex;
if (dot) dot.style.background = hex;
pickColor(null, hex);
}
function hexInputChanged(val) {
if (/^#[0-9a-fA-F]{6}$/.test(val) || /^#[0-9a-fA-F]{8}$/.test(val)) {
currentHexPreview = val;
const dot = document.getElementById('colorPreviewDot');
if (dot) dot.style.background = val;
const picker = document.getElementById('colorNativePicker');
if (picker) picker.value = val.substring(0, 7);
}
}
function applyColorPreview() { pickColor(null, currentHexPreview); }
function pickTransparent() {
const cta = findCta(ctaSelectedId); if (!cta) return;
cta.color = 'transparent'; cta.textColor = '#000';
refreshChipEl(cta);
}
function pickColor(el, color) {
if (el) {
const row = el.closest('.color-palette-row,.saved-palette-grid,.color-pane');
if (row) row.querySelectorAll('.color-swatch-item,.saved-color-item').forEach(s => s.classList.remove('active'));
el.classList.add('active');
}
const cta = findCta(ctaSelectedId);
if (cta) { cta.color = color; cta.gradient = null; refreshChipEl(cta); }
const dot = document.getElementById('colorPreviewDot');
if (dot) dot.style.background = color;
const inp = document.getElementById('colorHexInput');
if (inp) inp.value = color;
}
/* ── Style pickers — all trigger immediate refreshChipEl ── */
function pickShape(el, radius) {
el.closest('.ctaed-anim-row').querySelectorAll('.ctaed-anim-chip').forEach(b => b.classList.remove('active'));
el.classList.add('active');
const cta = findCta(ctaSelectedId);
if (cta) { cta.borderRadius = radius; refreshChipEl(cta); }
}
function pickSize(el, size) {
el.closest('.ctaed-anim-row').querySelectorAll('.ctaed-anim-chip').forEach(b => b.classList.remove('active'));
el.classList.add('active');
const cta = findCta(ctaSelectedId);
if (cta) { cta.size = size; refreshChipEl(cta); }
}
function pickIcon(el, iconCls) {
el.closest('.ctaed-anim-row').querySelectorAll('.ctaed-anim-chip').forEach(b => b.classList.remove('active'));
el.classList.add('active');
const cta = findCta(ctaSelectedId);
if (cta) { cta.iconCls = iconCls; refreshChipEl(cta); }
}
/* ── Animation pickers — immediate preview ── */
function pickAnim(el, anim) {
document.querySelectorAll('.ctaed-anim-chip-big').forEach(b => b.classList.remove('active'));
el.classList.add('active');
const cta = findCta(ctaSelectedId);
if (!cta) return;
cta.anim = anim;
const chipEl = document.getElementById('ctaChip' + cta.id);
if (chipEl) chipEl.className = 'ctaed-chip' + (ctaSelectedId === cta.id ? ' selected' : '') + animClass(cta);
}
function pickAnimSpeed(el, speed) {
el.closest('.ctaed-anim-row').querySelectorAll('.ctaed-anim-chip').forEach(b => b.classList.remove('active'));
el.classList.add('active');
const cta = findCta(ctaSelectedId);
if (!cta) return;
cta.animSpeed = speed;
const chipEl = document.getElementById('ctaChip' + cta.id);
if (chipEl) chipEl.className = 'ctaed-chip' + (ctaSelectedId === cta.id ? ' selected' : '') + animClass(cta);
}
/* ── Button link type toggle ── */
function setButtonLinkType(type, btn) {
const cta = findCta(ctaSelectedId); if (!cta) return;
cta.linkType = type;
document.querySelectorAll('#btnLinkUrl,#btnLinkPartner').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const urlBlock = document.getElementById('cfBtnUrlBlock');
const partnerBlock = document.getElementById('cfBtnPartnerBlock');
if (urlBlock) urlBlock.style.display = type === 'url' ? '' : 'none';
if (partnerBlock) partnerBlock.style.display = type === 'partner' ? '' : 'none';
refreshChipEl(cta);
}
/* ── Live preview: update chip as user types in content tab ── */
function ctaLiveUpdate(field, value) {
const cta = findCta(ctaSelectedId); if (!cta) return;
switch (field) {
case 'label': cta.label = value; break;
case 'title': cta.title = value; break;
case 'productName': cta.productName = value; break;
case 'price': cta.price = value; break;
case 'url': cta.url = value; break;
case 'tooltip': cta.tooltip = value; break;
case 'formTitle': cta.formTitle = value; break;
}
refreshChipEl(cta);
}
function saveCurrentColor() {
const cta = findCta(ctaSelectedId);
const color = cta ? (cta.gradient || cta.color) : currentHexPreview;
if (!savedColors.includes(color)) {
savedColors.unshift(color);
if (savedColors.length > 20) savedColors.pop();
localStorage.setItem('ctaSavedColors', JSON.stringify(savedColors));
toast('Color saved to palette ✓');
}
}
function clearSavedColors() { savedColors = []; localStorage.setItem('ctaSavedColors', '[]'); renderSavedPalette(); }
function renderSavedPalette() {
const grid = document.getElementById('savedPaletteGrid'); if (!grid) return;
grid.innerHTML = savedColors.map((c, i) => {
const bg = c.startsWith('linear') || c.startsWith('radial') ? c : 'background:' + c;
const style = c.startsWith('linear') || c.startsWith('radial') ? 'background:' + c : 'background:' + c;
return '
';
}).join('') + '
';
}
function applyFromSaved(i) {
const color = savedColors[parseInt(i)];
const cta = findCta(ctaSelectedId); if (!cta) return;
if (color.startsWith('linear') || color.startsWith('radial')) { cta.gradient = color; cta.color = color; }
else { cta.color = color; cta.gradient = null; }
refreshChipEl(cta);
toast('Style applied ✓');
}
/* Gradient */
function renderGradStops() {
const container = document.getElementById('gradStops'); if (!container) return;
container.innerHTML = gradStops.map((s, i) =>
'
' +
' ' +
' ' +
'% ' +
(i > 1 ? ' ' : '') +
'
'
).join('');
updateGradPreview();
}
function updateGradStop(i, key, val) { gradStops[i][key] = key === 'pct' ? parseInt(val) : val; updateGradPreview(); }
function addGradStop() { gradStops.push({ color: '#ffffff', pct: 50 }); renderGradStops(); }
function removeGradStop(i) { if (gradStops.length > 2) { gradStops.splice(i, 1); renderGradStops(); } }
function pickGradDir(el, dir) {
document.querySelectorAll('.grad-dir-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
gradDir = dir;
updateGradPreview();
}
function updateGradPreview() {
const stops = gradStops.map(s => s.color + ' ' + s.pct + '%').join(', ');
const grad = gradDir === 'radial'
? 'radial-gradient(circle, ' + stops + ')'
: 'linear-gradient(' + gradDir + ', ' + stops + ')';
const prev = document.getElementById('gradPreview');
if (prev) prev.style.background = grad;
}
function applyGradient() {
const stops = gradStops.map(s => s.color + ' ' + s.pct + '%').join(', ');
const grad = gradDir === 'radial'
? 'radial-gradient(circle, ' + stops + ')'
: 'linear-gradient(' + gradDir + ', ' + stops + ')';
const cta = findCta(ctaSelectedId); if (!cta) return;
cta.gradient = grad; cta.color = grad;
refreshChipEl(cta);
toast('Gradient applied ✓');
}
/* Saved button styles */
function saveCurrentStyle() {
const cta = findCta(ctaSelectedId); if (!cta) return;
const style = { color: cta.color, gradient: cta.gradient, borderRadius: cta.borderRadius, size: cta.size, iconCls: cta.iconCls, label: cta.label };
savedStyles.unshift(style);
if (savedStyles.length > 12) savedStyles.pop();
localStorage.setItem('ctaSavedStyles', JSON.stringify(savedStyles));
renderSavedStyles();
toast('Style saved ✓');
}
function renderSavedStyles() {
const grid = document.getElementById('savedStylesGrid'); if (!grid) return;
if (!savedStyles.length) { grid.innerHTML = '
No saved styles yet
'; return; }
grid.innerHTML = savedStyles.map((s, i) =>
'
' +
(s.label || '').substring(0, 10) + '
'
).join('');
}
function applyFromSavedStyle(i) {
const style = savedStyles[i];
const cta = findCta(ctaSelectedId); if (!cta) return;
Object.assign(cta, { color: style.color, gradient: style.gradient, borderRadius: style.borderRadius, size: style.size, iconCls: style.iconCls });
refreshChipEl(cta);
toast('Style applied ✓');
}
/* Override renderCtaStyleTab to use new color system */
function renderCtaStyleTab(cta) {
renderCtaColorSwatches();
renderSavedStyles();
// Init gradient stops from cta if gradient
if (cta.gradient) {
const match = cta.gradient.match(/#[0-9a-fA-F]{6}/g);
if (match && match.length >= 2) { gradStops = match.map((c, i) => ({ color: c, pct: Math.round(i / (match.length - 1) * 100) })); }
}
updateGradPreview();
}
/* ══════════════════════════════════════
CLIENT CTA ACTIONS — media, form, calendar
══════════════════════════════════════ */
function openVideo(title, url, dataUrl) {
(document.getElementById('ctaMediaTitle')||document.createElement('div')).textContent = '▶ ' + (title || 'Video');
const body = document.getElementById('ctaMediaBody');
const src = dataUrl || url || '';
if (src && (src.startsWith('blob:') || src.startsWith('data:video'))) {
body.innerHTML = `
`;
} else if (src && (src.includes('youtube') || src.includes('embed'))) {
body.innerHTML = `
`;
} else if (src) {
body.innerHTML = `
`;
} else {
body.innerHTML = `
`;
}
document.getElementById('ctaMediaModal').classList.add('open');
}
function openAudio(title, dataUrl) {
(document.getElementById('ctaMediaTitle')||document.createElement('div')).textContent = '🎵 ' + (title || 'Audio');
const body = document.getElementById('ctaMediaBody');
const src = dataUrl || '';
if (src) {
body.innerHTML = `
`;
} else {
body.innerHTML = '
' + (title || 'Audio') + '
0:00
';
}
document.getElementById('ctaMediaModal').classList.add('open');
}
function toggleAudioDemo() {
_audioPlaying = !_audioPlaying;
const ico = document.getElementById('ctaAudioIco');
if (ico) ico.className = 'ti ti-player-' + (_audioPlaying ? 'pause' : 'play');
if (_audioPlaying) {
_audioDemoTimer = setInterval(() => {
_audioPct += 0.5; if (_audioPct >= 100) _audioPct = 0;
const f = document.getElementById('ctaAudioFill'); if (f) f.style.width = _audioPct + '%';
const s = Math.round(_audioPct / 100 * 180);
const t = document.getElementById('ctaAudioTime'); if (t) t.textContent = Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
}, 100);
} else clearInterval(_audioDemoTimer);
}
function closeMediaModal() {
document.getElementById('ctaMediaModal').classList.remove('open');
_audioPlaying = false; clearInterval(_audioDemoTimer); _audioPct = 0;
}
function openLeadForm(title) {
const el = document.getElementById('ctaFormTitle');
if (el) el.textContent = title || 'Get in Touch';
document.getElementById('ctaFormModal').classList.remove('hidden');
}
function submitLeadForm() {
const n = document.getElementById('ctaFormName');
document.getElementById('ctaFormModal').classList.add('hidden');
toast('Thanks ' + (n ? n.value : '') + '! We\'ll be in touch ✓');
}
/* ══════════════════════════════════════
FULL QUIZ ENGINE (client view)
══════════════════════════════════════ */
let quizState = { questions: [], currentQ: 0, correct: 0, wrong: 0, pts: 0, answered: false, name: 'You', settings: {} };
function openQuizFull(questions, title, settings, userName) {
quizState = { questions, currentQ: 0, correct: 0, wrong: 0, pts: 0, answered: false, name: userName || 'You', settings: settings || {} };
(document.getElementById('quizFullTitle')||document.createElement('div')).textContent = title || 'Quiz';
(document.getElementById('quizQuestionView')||{}).style.display = '';
(document.getElementById('quizResultView')||{}).style.display = 'none';
renderQuizQuestion();
document.getElementById('quizModalFull').classList.add('open');
}
function closeQuizFull() { document.getElementById('quizModalFull').classList.remove('open'); stopConfetti(); }
function renderQuizQuestion() {
const qs = quizState.questions;
const qi = quizState.currentQ;
const q = qs[qi];
const total = qs.length;
const pct = (qi / total) * 100;
(document.getElementById('quizProgFill')||{}).style.width = pct + '%';
(document.getElementById('quizQNum')||document.createElement('div')).textContent = 'Q ' + (qi + 1) + ' of ' + total;
(document.getElementById('quizLiveScore')||document.createElement('div')).textContent = quizState.pts;
(document.getElementById('quizQLabel')||document.createElement('div')).textContent = 'Question ' + (qi + 1);
(document.getElementById('quizQText')||document.createElement('div')).textContent = q.text || 'Question';
const letters = ['A', 'B', 'C', 'D', 'E'];
(document.getElementById('quizOpts')||document.createElement('div')).innerHTML = (q.options || []).map((o, i) =>
'
' +
'
' + letters[i] + '
' + o + '
'
).join('');
document.getElementById('quizFeedback').className = 'quiz-feedback';
document.getElementById('quizNextBtn').disabled = true;
(document.getElementById('quizSkipBtn')||{}).style.display = '';
quizState.answered = false;
}
function selectQuizAnswer(oi) {
if (quizState.answered) return;
quizState.answered = true;
const q = quizState.questions[quizState.currentQ];
const isCorrect = oi === q.correctIdx;
const box = document.getElementById('quizBoxFull');
// Mark options
document.querySelectorAll('.quiz-opt').forEach((el, i) => {
if (i === q.correctIdx) el.classList.add('correct');
else if (i === oi && !isCorrect) el.classList.add('wrong');
});
// Score
if (isCorrect) { quizState.correct++; quizState.pts += 10; }
else { quizState.wrong++; }
(document.getElementById('quizLiveScore')||document.createElement('div')).textContent = quizState.pts;
// Feedback banner
const fb = document.getElementById('quizFeedback');
fb.className = 'quiz-feedback show ' + (isCorrect ? 'correct-fb' : 'wrong-fb');
(document.getElementById('quizFeedbackIco')||document.createElement('div')).textContent = isCorrect ? '🎉' : '❌';
(document.getElementById('quizFeedbackTitle')||document.createElement('div')).textContent = isCorrect ? 'Correct! +10 pts' : 'Not quite!';
(document.getElementById('quizFeedbackSub')||document.createElement('div')).textContent = q.explanation || (isCorrect ? 'Great work, keep it up!' : 'The correct answer is highlighted.');
// Flash + sound
box.classList.add(isCorrect ? 'flash-correct' : 'flash-wrong');
setTimeout(() => box.classList.remove('flash-correct', 'flash-wrong'), 700);
playQuizSound(isCorrect);
if (isCorrect && quizState.settings.soundFx !== false) startConfetti();
// Enable next
document.getElementById('quizNextBtn').disabled = false;
(document.getElementById('quizNextBtn')||document.createElement('div')).innerHTML = quizState.currentQ < quizState.questions.length - 1
? '
Next Question'
: '
See Results';
(document.getElementById('quizSkipBtn')||{}).style.display = 'none';
}
function quizNext() {
stopConfetti();
if (quizState.currentQ < quizState.questions.length - 1) {
quizState.currentQ++;
renderQuizQuestion();
} else {
showQuizResults();
}
}
function quizSkip() { quizState.wrong++; quizNext(); }
function restartQuiz() {
quizState.currentQ = 0; quizState.correct = 0; quizState.wrong = 0; quizState.pts = 0;
(document.getElementById('quizResultView')||{}).style.display = 'none';
(document.getElementById('quizQuestionView')||{}).style.display = '';
renderQuizQuestion();
}
function showQuizResults() {
(document.getElementById('quizQuestionView')||{}).style.display = 'none';
(document.getElementById('quizResultView')||{}).style.display = '';
const total = quizState.questions.length;
const pct = Math.round(quizState.correct / total * 100);
(document.getElementById('quizScorePct')||document.createElement('div')).textContent = pct + '%';
(document.getElementById('quizResCorrect')||document.createElement('div')).textContent = quizState.correct;
(document.getElementById('quizResWrong')||document.createElement('div')).textContent = quizState.wrong;
(document.getElementById('quizResPts')||document.createElement('div')).textContent = quizState.pts;
// Score ring
const deg = Math.round(pct / 100 * 360);
(document.getElementById('quizScoreRing')||{}).style.background = 'conic-gradient(var(--gold) ' + deg + 'deg, var(--border) 0deg)';
// Trophy + title
const trophy = pct >= 80 ? '🏆' : pct >= 50 ? '🥈' : '📚';
const title = pct >= 80 ? 'Excellent!' : pct >= 50 ? 'Good Job!' : 'Keep Learning!';
(document.getElementById('quizResultTrophy')||document.createElement('div')).textContent = trophy;
(document.getElementById('quizResultTitle')||document.createElement('div')).textContent = title;
(document.getElementById('quizResultSub')||document.createElement('div')).textContent = quizState.correct + ' of ' + total + ' correct · ' + quizState.pts + ' points earned';
// Show/hide buttons based on settings
const certBtn = document.querySelector('.quiz-action-btn.primary');
const lbBtn = document.querySelector('.quiz-action-btn.secondary');
if (certBtn) certBtn.style.display = quizState.settings.certificate !== false ? '' : 'none';
if (lbBtn) lbBtn.style.display = quizState.settings.leaderboard !== false ? '' : 'none';
// Add to leaderboard
addToLeaderboard(quizState.name, quizState.pts, quizState.correct, total);
if (pct >= 70) startConfetti();
}
/* ══ Web Audio sound FX ══ */
let audioCtx = null;
function getAudioCtx() { if (!audioCtx) try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { } return audioCtx; }
function playQuizSound(correct) {
if (quizState.settings && quizState.settings.soundFx === false) return;
const ctx = getAudioCtx(); if (!ctx) return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
if (correct) {
// Ascending happy chime
osc.type = 'sine';
osc.frequency.setValueAtTime(523, ctx.currentTime);
osc.frequency.setValueAtTime(659, ctx.currentTime + 0.1);
osc.frequency.setValueAtTime(784, ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
osc.start(); osc.stop(ctx.currentTime + 0.5);
} else {
// Descending sad tone
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(330, ctx.currentTime);
osc.frequency.setValueAtTime(220, ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
osc.start(); osc.stop(ctx.currentTime + 0.5);
}
}
/* ══ Confetti ══ */
let confettiAnimId = null;
const CONFETTI_COLORS = ['#C8973A', '#22C55E', '#3B82F6', '#EF4444', '#8B5CF6', '#F97316', '#EC4899'];
let confettiParticles = [];
function startConfetti() {
const canvas = document.getElementById('quizConfetti');
if (!canvas) return;
canvas.style.display = 'block';
canvas.width = window.innerWidth; canvas.height = window.innerHeight;
confettiParticles = Array.from({ length: 80 }, () => ({
x: Math.random() * canvas.width, y: -10,
w: 8 + Math.random() * 8, h: 4 + Math.random() * 4,
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
vx: (Math.random() - 0.5) * 4, vy: 2 + Math.random() * 4,
rot: Math.random() * 360, vr: (Math.random() - 0.5) * 8, opacity: 1
}));
cancelAnimationFrame(confettiAnimId);
animateConfetti();
setTimeout(stopConfetti, 3500);
}
function animateConfetti() {
const canvas = document.getElementById('quizConfetti'); if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
confettiParticles.forEach(p => {
p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.vy += 0.05;
if (p.y > canvas.height + 10) { p.y = -10; p.x = Math.random() * canvas.width; }
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot * Math.PI / 180);
ctx.fillStyle = p.color; ctx.globalAlpha = Math.max(0, p.opacity);
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); ctx.restore();
});
confettiAnimId = requestAnimationFrame(animateConfetti);
}
function stopConfetti() {
cancelAnimationFrame(confettiAnimId);
const canvas = document.getElementById('quizConfetti');
if (canvas) { canvas.style.display = 'none'; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); }
}
/* ══════════════════════════════════════
LEADERBOARD
══════════════════════════════════════ */
let leaderboardData = JSON.parse(localStorage.getItem('quizLeaderboard') || '[]');
function addToLeaderboard(name, pts, correct, total) {
leaderboardData.unshift({ name, pts, correct, total, date: Date.now(), ini: name.split(' ').map(w => w[0]).join('').toUpperCase().substring(0, 2) });
if (leaderboardData.length > 50) leaderboardData.pop();
localStorage.setItem('quizLeaderboard', JSON.stringify(leaderboardData));
}
function openLeaderboard() {
document.getElementById('lbModal').classList.add('open');
setLbTab('all', document.querySelector('.lb-tab'));
}
function setLbTab(tab, el) {
document.querySelectorAll('.lb-tab').forEach(t => t.classList.remove('active'));
if (el) el.classList.add('active');
const body = document.getElementById('lbBody');
let data = [...leaderboardData].sort((a, b) => b.pts - a.pts);
const rankColors = ['gold', 'silver', 'bronze'];
if (tab === 'week') { const week = Date.now() - 7 * 864e5; data = data.filter(d => d.date > week); }
if (tab === 'me') {
const myPts = quizState.pts;
const myRank = data.findIndex(d => d.pts <= myPts) + 1 || data.length + 1;
body.innerHTML = '
🏅
Rank #' + myRank + '
You scored ' + myPts + ' pts · ' + quizState.correct + ' correct
';
return;
}
if (!data.length) { body.innerHTML = '
No scores yet — be the first!
'; return; }
const maxPts = data[0].pts || 1;
body.innerHTML = data.slice(0, 20).map((d, i) => {
const isMe = d.pts === quizState.pts && d.name === quizState.name;
return '
' +
'
' + (i < 3 ? ['🥇', '🥈', '🥉'][i] : (i + 1)) + '
' +
'
' + (d.ini || 'U') + '
' +
'
' + d.name + (isMe ? ' (You)' : '') + '
' + d.correct + '/' + d.total + ' correct
' +
'
' +
'
' +
'
';
}).join('');
}
/* ══════════════════════════════════════
CERTIFICATE
══════════════════════════════════════ */
function openCertificate() {
const pct = Math.round(quizState.correct / quizState.questions.length * 100);
const modal = document.getElementById('certModal');
modal.classList.add('open');
drawCertificate(quizState.name, quizState.pts, pct, quizState.correct, quizState.questions.length);
}
function drawCertificate(name, pts, pct, correct, total) {
const canvas = document.getElementById('certCanvas');
const ctx = canvas.getContext('2d');
const W = 700, H = 490;
// Background
const bgGrad = ctx.createLinearGradient(0, 0, W, H);
bgGrad.addColorStop(0, '#0F1B2D'); bgGrad.addColorStop(1, '#1A2D45');
ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, W, H);
// Gold border
ctx.strokeStyle = '#C8973A'; ctx.lineWidth = 3;
ctx.strokeRect(16, 16, W - 32, H - 32);
ctx.strokeStyle = 'rgba(200,151,58,.3)'; ctx.lineWidth = 1;
ctx.strokeRect(24, 24, W - 48, H - 48);
// Corner ornaments
[[[16, 16], [40, 16], [16, 40]], [[W - 16, 16], [W - 40, 16], [W - 16, 40]], [[16, H - 16], [40, H - 16], [16, H - 40]], [[W - 16, H - 16], [W - 40, H - 16], [W - 16, H - 40]]].forEach(pts => {
ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]); pts.slice(1).forEach(p => ctx.lineTo(p[0], p[1]));
ctx.strokeStyle = '#C8973A'; ctx.lineWidth = 2; ctx.stroke();
});
// Trophy
ctx.font = '52px serif'; ctx.textAlign = 'center';
ctx.fillText(pct >= 80 ? '🏆' : pct >= 50 ? '🥈' : '🎖️', W / 2, 95);
// Title
ctx.fillStyle = '#E8B55A'; ctx.font = 'bold 14px DM Sans,sans-serif'; ctx.letterSpacing = '3px';
ctx.fillText('CERTIFICATE OF ACHIEVEMENT', W / 2, 132);
ctx.letterSpacing = '0px';
// Subtitle
ctx.fillStyle = 'rgba(255,255,255,.5)'; ctx.font = '12px DM Sans,sans-serif';
ctx.fillText('This certifies that', W / 2, 158);
// Name
ctx.fillStyle = 'white'; ctx.font = 'bold 34px DM Serif Display,Georgia,serif';
ctx.fillText(name || 'Learner', W / 2, 202);
// Gold line
const gl = ctx.createLinearGradient(150, 0, 550, 0);
gl.addColorStop(0, 'transparent'); gl.addColorStop(0.5, '#C8973A'); gl.addColorStop(1, 'transparent');
ctx.fillStyle = gl; ctx.fillRect(150, 212, 400, 2);
// Achievement text
ctx.fillStyle = 'rgba(255,255,255,.6)'; ctx.font = '13px DM Sans,sans-serif';
ctx.fillText('has successfully completed the quiz with', W / 2, 240);
// Score
ctx.fillStyle = '#E8B55A'; ctx.font = 'bold 26px DM Sans,sans-serif';
ctx.fillText(pct + '% Score · ' + correct + '/' + total + ' Correct · ' + pts + ' Points', W / 2, 275);
// Stats row
ctx.fillStyle = 'rgba(255,255,255,.35)'; ctx.font = '11px DM Sans,sans-serif';
ctx.fillText('Issued: ' + new Date().toLocaleDateString('en-IN', { year: 'numeric', month: 'long', day: 'numeric' }), W / 2, 310);
// Bottom brand
ctx.fillStyle = 'rgba(255,255,255,.2)'; ctx.font = '10px DM Sans,sans-serif';
ctx.fillText('Powered by Sales Playbook Platform', W / 2, H - 36);
// Seal circle
ctx.beginPath(); ctx.arc(W / 2, 370, 36, 0, Math.PI * 2);
const sg = ctx.createRadialGradient(W / 2, 370, 0, W / 2, 370, 36);
sg.addColorStop(0, '#E8B55A'); sg.addColorStop(1, '#C8973A');
ctx.fillStyle = sg; ctx.fill();
ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#0F1B2D'; ctx.font = 'bold 11px DM Sans,sans-serif';
ctx.fillText('VERIFIED', W / 2, 366);
ctx.font = '9px DM Sans,sans-serif';
ctx.fillText('' + pct + '%', W / 2, 380);
}
function downloadCert() {
const canvas = document.getElementById('certCanvas');
const link = document.createElement('a');
link.download = 'certificate-' + (quizState.name || 'learner').replace(/\s+/g, '-') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
function shareCert() {
const canvas = document.getElementById('certCanvas');
canvas.toBlob(blob => {
if (navigator.share && navigator.canShare && navigator.canShare({ files: [new File([blob], 'certificate.png', { type: 'image/png' })] })) {
navigator.share({ title: 'My Quiz Certificate', text: 'I scored ' + quizState.pts + ' points!', files: [new File([blob], 'certificate.png', { type: 'image/png' })] });
} else {
toast('Copy the image to share on social media 📤');
}
});
}
/* ── Wire openQuiz to new engine ── */
function openQuiz(question, options, correct, quizCta) {
// Support both old single-Q format and new multi-Q
if (quizCta && quizCta.questions && quizCta.questions.length) {
openQuizFull(quizCta.questions, quizCta.label, quizCta.quizSettings, 'You');
} else {
const singleQ = [{ text: question, options: options || [], correctIdx: (options || []).indexOf(correct), explanation: '' }];
openQuizFull(singleQ, 'Quick Quiz', {}, 'You');
}
}
←
Prev
→
Next
Space
Auto
⌘
+/−
Zoom
🔄
360° Product View
EMBED
How would you like to view this?
Choose your preferred experience
Browse
Scroll through at your own pace. Click any document to open it.
Slideshow
Auto-advances through all pages hands-free. Adjust speed anytime.
Remember my preference
Continue
Q2 Product Deck
0.5×
1×
1.5×
2×
Browse mode
Done with Q2 Product Deck
Up next: Pricing Sheet Q2
3
Starting next document…
Skip ahead
Stay here
Pick a date
Click any highlighted date on the calendar to see
available time slots
My availability
Click slots below to mark as
busy/free. Clients only see free slots.
Default meeting
duration
15m
30m
45m
60m
🎉
Meeting Booked!
📅 Added to calendar
📧 Confirmation details below
Done
Notifications
Mark
all read
All
Messages
Bookings
Activity
Meetings
Upcoming calls, past recordings, AI recaps
Book
Availability
Upcoming
Past
Opening
Signals
Questions
Talking Points
Watch out
Select a prospect to generate
their brief.
Mutual Action Plan
Shared with Priya Sharma · FinCorp India
Add item
Deal Room
Private workspace
Playbook
Workspace
Overview
Views & Sessions
Documents
Meetings
Proposals
Signed Docs
Action Plan
Threads
Timeline
Buying Committee
Smart Nudge Engine
AI decides when prospects are in a decision window
Refresh
Sending
nudge to Priya Sharma
Offline Mode
Download playbooks for field demos · syncs when back online
Download
Save playbook as a self-contained HTML file
Present offline
Open file anywhere — no internet needed
Auto-sync
All interactions tracked and synced when back online
Choose playbook to download
Download options
Include all document previews
Embeds page thumbnails for offline viewing (+2MB
approx.)
Track interactions offline
Records opens, time-on-page, and clicks in browser
storage
Auto-sync on reconnect
Sends tracked events to server when internet resumes
Expiry protection
Offline file respects link expiry settings
Sync now
Download Offline Playbook
Sign Document
Q2 Enterprise Proposal
Signing as
RN
Rahul Nair
Sales Manager, effyBiz
Type
Draw
Upload
Your full name
Choose a style:
Your Name
Your Name
Your Name
Draw your signature in the box below:
Clear
By clicking Sign Document , you agree that this electronic signature is legally binding
and equivalent to a handwritten signature under applicable law.
Sign Document
Call
WhatsApp
Email
Log
Reminder
Update Status
Overview
Signals
Sales Kit
Roleplay
AI
New Demo Journey
Step 1 of 5 · Info
Info
Content
Registration
Share
Analytics
Signed Documents
All fully executed agreements and proposals
Invite AI Notetaker
Meeting
Joins as "effyBiz
Notetaker"
Meeting link
When should the bot join?
Now
Call in progress
At meeting time
2 min before start
Custom
Pick a time
The bot will capture:
Full transcript
Key decisions
Action items
Sentiment & talk time
Open questions
Next steps
The bot joins as "effyBiz
Notetaker" — visible to all participants. Everyone in the call will know it is being
recorded.
Cancel
Invite Bot Now
Modules & Settings
Enable or disable optional features
Core features are always on. Toggle optional
modules to customise your workspace.
AI Meeting Bot
Joins calls, records audio, generates AI recap
Not connected
Save
& Test
Deploy the meeting bot server once → paste the URL here → all agents share it automatically.
Deploy
guide →
Done
Create New Team
Set up team and assign modules
Cancel
Save Team
Add Team Member
Configure access and module visibility
Role
Admin
Sales Rep
Agent
Client
Assign
to Team
Hidden
Modules
Select modules to hide from this member
(even if their team has access)
Cancel
Add Member
Manage Modules
Toggle module access for this team
Select which modules can access
Cancel
Save Changes
Leads
People who engaged with your content
All
Hot
Warm
Cold
New
By intent
Most recent
Most views
Upload Collateral
Add from your PC, Google Drive, or SharePoint
From PC
Google Drive
SharePoint
Web Link
Add a link to your
library
YouTube · Loom · Website · Portfolio · Google Doc · Figma
· Any URL
Quick presets
YouTube video
Loom recording
Figma design
Google Slides
Portfolio site
Case study
Add to Library
Drag & drop files here
or click to browse your computer
PDF
PPTX
XLSX
DOCX
MP4
PNG/JPG
Browse Google Drive
Pick files directly from your Drive
Needs
API key
Paste a Google Drive link
No
setup needed
SharePoint / OneDrive
Paste a sharing link from SharePoint or OneDrive
Full SharePoint integration
For automated sync with SharePoint document
libraries, the admin can configure the Microsoft Graph API integration in Settings →
Integrations .
Cancel
Add to Library
Quick Share
1 collateral selected
Build Playbook instead
Details
Landing Page
Security
Send via
WhatsApp
Email
Copy link
Send to
Press Enter to add each email address
Personal message optional
Viewing mode
A branded landing page
wraps your document. The recipient opens this page first before viewing the file.
Choose template
Preview
Link expiry
One time
1 day
7 days
14 days
30 days
Custom
Never
Watermark
Show viewer name as watermark
Displays recipient name on each page