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.');
});
});
})();