var STUDIO_PHONE = '541-658-8030'; // Update this if the Worker moves to a custom domain. var WORKER_BASE_URL = "https://nsds-bring-a-friend.josh-d65.workers.dev"; document.documentElement.style.setProperty('--hero-photo', 'url(' + HERO_SRC + ')'); var params = new URLSearchParams(window.location.search); var prefillDancer = params.get('dancer') || ''; var contactId = params.get('cid') || ''; var canvas = document.getElementById('invite-canvas'); var ctx = canvas.getContext('2d'); var nameInput = document.getElementById('dancer-name-input'); if (prefillDancer) nameInput.value = prefillDancer; var smsBtn = document.getElementById('share-sms-btn'); var fbBtn = document.getElementById('share-fb-btn'); var igBtn = document.getElementById('share-ig-btn'); var statusEl = document.getElementById('status-line'); var logoImg = new Image(); var heroImg = new Image(); var assetsReady = 0; function onAssetLoad() { assetsReady++; if (assetsReady === 2) draw(); } logoImg.onload = onAssetLoad; heroImg.onload = onAssetLoad; logoImg.src = LOGO_SRC; heroImg.src = HERO_SRC; function wrapText(context, text, x, y, maxWidth, lineHeight) { var words = text.split(' '); var line = ''; var lines = []; for (var n = 0; n < words.length; n++) { var testLine = line + words[n] + ' '; if (context.measureText(testLine).width > maxWidth && n > 0) { lines.push(line.trim()); line = words[n] + ' '; } else { line = testLine; } } lines.push(line.trim()); lines.forEach(function (l, i) { context.fillText(l, x, y + i * lineHeight); }); return lines.length; } function roundRect(context, x, y, w, h, r) { context.beginPath(); context.moveTo(x + r, y); context.arcTo(x + w, y, x + w, y + h, r); context.arcTo(x + w, y + h, x, y + h, r); context.arcTo(x, y + h, x, y, r); context.arcTo(x, y, x + w, y, r); context.closePath(); } function ticketNotches(context, x, w, y, bg) { context.save(); context.fillStyle = bg; context.beginPath(); context.arc(x, y, 22, 0, Math.PI * 2); context.fill(); context.beginPath(); context.arc(x + w, y, 22, 0, Math.PI * 2); context.fill(); context.restore(); context.save(); context.strokeStyle = 'rgba(255,255,255,0.35)'; context.lineWidth = 3; context.setLineDash([14, 14]); context.beginPath(); context.moveTo(x + 40, y); context.lineTo(x + w - 40, y); context.stroke(); context.setLineDash([]); context.restore(); } function draw() { var w = canvas.width, h = canvas.height; ctx.clearRect(0, 0, w, h); var iw = heroImg.width, ih = heroImg.height; var scale = Math.max(w / iw, h / ih); var dw = iw * scale, dh = ih * scale; var dx = (w - dw) / 2, dy = (h - dh) / 2 - 160; ctx.drawImage(heroImg, dx, dy, dw, dh); var grad = ctx.createLinearGradient(0, 0, 0, h); grad.addColorStop(0, 'rgba(8,6,4,0.5)'); grad.addColorStop(0.42, 'rgba(8,6,4,0.8)'); grad.addColorStop(0.6, 'rgba(8,6,4,0.94)'); grad.addColorStop(1, 'rgba(8,6,4,0.97)'); ctx.fillStyle = grad; ctx.fillRect(0, 0, w, h); var cx = w / 2; var frameX = 24, frameY = 24, frameW = w - 48, frameH = h - 48; ctx.strokeStyle = 'rgba(212,175,55,0.85)'; ctx.lineWidth = 5; var cl = 46; [[frameX, frameY, 1, 1], [frameX + frameW, frameY, -1, 1], [frameX, frameY + frameH, 1, -1], [frameX + frameW, frameY + frameH, -1, -1]].forEach(function (c) { ctx.beginPath(); ctx.moveTo(c[0] + cl * c[2], c[1]); ctx.lineTo(c[0], c[1]); ctx.lineTo(c[0], c[1] + cl * c[3]); ctx.stroke(); }); ctx.strokeStyle = 'rgba(212,175,55,0.55)'; ctx.lineWidth = 3; roundRect(ctx, frameX, frameY, frameW, frameH, 36); ctx.stroke(); ctx.save(); ctx.translate(w - 210, 118); ctx.rotate(Math.PI / 4); var ribbonGrad = ctx.createLinearGradient(-160, 0, 160, 0); ribbonGrad.addColorStop(0, '#a9822a'); ribbonGrad.addColorStop(0.5, '#e7c355'); ribbonGrad.addColorStop(1, '#a9822a'); ctx.fillStyle = ribbonGrad; ctx.fillRect(-170, -30, 340, 60); ctx.fillStyle = '#201a0c'; ctx.font = '800 30px Nunito, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('FREE CLASS', 0, 2); ctx.restore(); ctx.textBaseline = 'alphabetic'; var logoW = w * 0.36; var logoH = logoW * (logoImg.height / logoImg.width); ctx.drawImage(logoImg, cx - logoW / 2, 118, logoW, logoH); var yCursor = 118 + logoH + 56; ctx.font = '700 28px Nunito, sans-serif'; var badgeText = '✷ BRING A FRIEND WEEK'; var badgeW = ctx.measureText(badgeText).width + 66; ctx.fillStyle = 'rgba(255,255,255,0.1)'; roundRect(ctx, cx - badgeW / 2, yCursor, badgeW, 58, 29); ctx.fill(); ctx.strokeStyle = 'rgba(212,175,55,0.6)'; ctx.lineWidth = 2; roundRect(ctx, cx - badgeW / 2, yCursor, badgeW, 58, 29); ctx.stroke(); ctx.fillStyle = '#f3e3ae'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(badgeText, cx, yCursor + 31); ctx.textBaseline = 'alphabetic'; yCursor += 58 + 62; ctx.save(); ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 22; ctx.fillStyle = '#ffffff'; ctx.font = '800 104px Fraunces, Georgia, serif'; ctx.textAlign = 'center'; ctx.fillText("You're Invited!", cx, yCursor + 86); ctx.restore(); yCursor += 86 + 76; var dancerName = (nameInput.value || 'A friend').trim(); if (dancerName.length === 0) dancerName = 'A friend'; ctx.fillStyle = '#f3e3ae'; ctx.font = 'italic 700 56px Fraunces, Georgia, serif'; var inviteLine = dancerName + ' invited you to a FREE dance class!'; var lineCount = wrapText(ctx, inviteLine, cx, yCursor, w - 190, 70); yCursor += lineCount * 70 + 40; ctx.fillStyle = 'rgba(255,255,255,0.88)'; ctx.font = '600 36px Nunito, sans-serif'; ctx.fillText('No cost, no strings — just come dance with a friend.', cx, yCursor); var notchY = h - 250; ticketNotches(ctx, frameX, frameW, notchY, '#0b0906'); ctx.fillStyle = 'rgba(255,255,255,0.78)'; ctx.font = '800 36px Nunito, sans-serif'; ctx.fillText('Next Step Dance Studio', cx, h - 168); ctx.font = '600 30px Nunito, sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.fillText('Springfield, OR', cx, h - 122); ctx.font = '700 30px Nunito, sans-serif'; ctx.fillStyle = '#f3e3ae'; ctx.fillText(STUDIO_PHONE, cx, h - 74); ctx.textAlign = 'left'; } nameInput.addEventListener('input', draw); // Registers (or fetches) the reusable invite for the current name via the // Worker, so SMS/Facebook can link to something with a real, personalized // og:image instead of GHL's generic page preview. var lastInviteName = null; var inviteDataPromise = null; function getInviteData() { var dancerName = (nameInput.value || 'A friend').trim(); if (dancerName.length === 0) dancerName = 'A friend'; if (lastInviteName === dancerName && inviteDataPromise) return inviteDataPromise; lastInviteName = dancerName; inviteDataPromise = fetch(WORKER_BASE_URL + '/api/invites', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dancer_name: dancerName, contact_id: contactId || undefined }), }) .then(function (res) { return res.json(); }) .then(function (data) { if (!data.ok) throw new Error(data.error || 'Could not create invite'); return data; // { slug, dancerName, shareUrl, ogImageUrl, qrUrl } }); return inviteDataPromise; } function getShareText(shareUrl) { var dancerName = (nameInput.value || 'A friend').trim(); if (dancerName.length === 0) dancerName = 'A friend'; return dancerName + ' invited you to a FREE dance class at Next Step Dance during Bring a Friend Week! ' + shareUrl; } function canvasToFile() { return new Promise(function (resolve) { canvas.toBlob(function (blob) { var dancerName = (nameInput.value || 'invite').trim().replace(/\s+/g, '-').toLowerCase() || 'invite'; resolve(new File([blob], 'bring-a-friend-' + dancerName + '.png', { type: 'image/png' })); }, 'image/png'); }); } function setStatus(msg, cls) { statusEl.textContent = msg; statusEl.className = 'status-line' + (cls ? ' ' + cls : ''); } var shareFilesSupported = !!(navigator.canShare && navigator.share); function requireName() { if (!nameInput.value.trim()) { nameInput.focus(); return false; } return true; } function nativeShare(shareUrl, extra) { return canvasToFile().then(function (file) { var shareData = { files: [file], title: 'Bring a Friend Week', text: getShareText(shareUrl) }; if (shareFilesSupported && navigator.canShare(shareData)) { return navigator.share(shareData).then(function () { setStatus('Shared! 🎉', 'ok'); }).catch(function (err) { if (err && err.name === 'AbortError') setStatus(''); else setStatus(''); }); } if (extra) extra(); return null; }); } smsBtn.addEventListener('click', function () { if (!requireName()) return; setStatus('Preparing…'); getInviteData().then(function (data) { var body = encodeURIComponent(getShareText(data.shareUrl)); setStatus(''); window.location.href = 'sms:?&body=' + body; }).catch(function () { setStatus('Could not prepare invite — please try again.'); }); }); fbBtn.addEventListener('click', function () { if (!requireName()) return; setStatus('Preparing…'); getInviteData().then(function (data) { var url = 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(data.shareUrl); setStatus(''); window.open(url, '_blank', 'noopener,width=600,height=600'); }).catch(function () { setStatus('Could not prepare invite — please try again.'); }); }); igBtn.addEventListener('click', function () { if (!requireName()) return; setStatus('Preparing…'); getInviteData().then(function (data) { return nativeShare(data.shareUrl, function () { var text = getShareText(data.shareUrl); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { setStatus('Caption copied — paste it into Instagram', 'ok'); }); } else { setStatus('Open Instagram and paste the invite link to share.'); } }); }).catch(function () { setStatus('Could not prepare invite — please try again.'); }); }); })();