diff --git a/public/announce.js b/public/announce.js index 5c158bd..bffca22 100644 --- a/public/announce.js +++ b/public/announce.js @@ -1,202 +1,208 @@ -/* announce.js โ€” dismissible event pop-up overlay (RM Circle). - Content is admin-controlled: fetched from /api/announce (edit it under - Admin โ†’ Settings โ†’ Event pop-up; no deploy needed). Self-gating: shows once - per browser until dismissed, and never after `expiresUTC`. */ -(function () { - function parseTimes(s) { - return String(s || '').split(/\r?\n+/).map(function (l) { return l.trim(); }).filter(Boolean) - .map(function (l) { var i = l.indexOf('|'); return i < 0 ? [l, ''] : [l.slice(0, i).trim(), l.slice(i + 1).trim()]; }); - } - - function start(EV) { - if (!EV || !EV.enabled || !EV.id || !EV.img) return false; - if (EV.expiresUTC) { var exp = Date.parse(EV.expiresUTC); if (exp && Date.now() > exp) return false; } - var KEY = 'rmc-announce-' + EV.id; - try { if (localStorage.getItem(KEY) === 'done') return false; } catch (e) {} - EV.timesArr = parseTimes(EV.times); - - var back, card; - function done() { try { localStorage.setItem(KEY, 'done'); } catch (e) {} } - function close() { - if (!back) return; - back.style.opacity = '0'; - setTimeout(function () { if (back && back.parentNode) back.parentNode.removeChild(back); back = null; }, 260); - document.removeEventListener('keydown', onKey); - } - function dismiss() { done(); close(); } - function onKey(e) { if (e.key === 'Escape') dismiss(); } - - function build() { - back = document.createElement('div'); - back.setAttribute('role', 'dialog'); - back.setAttribute('aria-label', (EV.eyebrow || 'Event') + ' invitation'); - back.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;' + - 'padding:20px;background:rgba(3,7,14,.82);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);' + - 'opacity:0;transition:opacity .28s ease;overflow:auto;'; - back.addEventListener('click', function (e) { if (e.target === back) dismiss(); }); - - card = document.createElement('div'); - card.style.cssText = 'position:relative;width:100%;max-width:428px;max-height:94vh;overflow:auto;border-radius:20px;' + - 'background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7),0 0 0 1px rgba(212,175,55,.15);' + - 'font-family:system-ui,Segoe UI,Arial,sans-serif;'; - - var x = document.createElement('button'); - x.setAttribute('aria-label', 'Close'); - x.innerHTML = '×'; - x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:38px;height:38px;border-radius:50%;border:none;cursor:pointer;' + - 'background:rgba(6,10,16,.72);color:#fff;font-size:24px;line-height:1;font-weight:700;display:grid;place-items:center;box-shadow:0 2px 10px rgba(0,0,0,.5);'; - x.addEventListener('click', dismiss); - - var eyebrow = document.createElement('div'); - eyebrow.textContent = EV.eyebrow || ''; - eyebrow.style.cssText = 'text-align:center;letter-spacing:2px;text-transform:uppercase;font-size:11px;font-weight:700;color:#d4af37;padding:14px 44px 10px;'; - - var img = document.createElement('img'); - img.src = EV.img; - img.alt = EV.eyebrow || 'Event flyer'; - img.style.cssText = 'display:block;width:100%;height:auto;'; - - var foot = document.createElement('div'); - foot.style.cssText = 'padding:16px 20px 20px;'; - - if (EV.dateLabel) { - var dateRow = document.createElement('div'); - dateRow.innerHTML = '๐Ÿ—“ '; - dateRow.querySelector('b').textContent = EV.dateLabel; - dateRow.style.cssText = 'text-align:center;color:#eef2f7;font-size:15px;margin-bottom:12px;'; - foot.appendChild(dateRow); - } - - if (EV.timesArr.length) { - var grid = document.createElement('div'); - grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;'; - EV.timesArr.forEach(function (t) { - var cell = document.createElement('div'); - cell.style.cssText = 'background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.22);border-radius:10px;padding:8px 10px;'; - var time = document.createElement('div'); - time.style.cssText = 'font-size:15px;font-weight:800;color:#f3c34a;'; time.textContent = t[0]; - var region = document.createElement('div'); - region.style.cssText = 'font-size:11px;color:#9fb0c2;margin-top:1px;'; region.textContent = t[1] || 'ย '; - cell.appendChild(time); cell.appendChild(region); grid.appendChild(cell); - }); - foot.appendChild(grid); - } - - var join = document.createElement('a'); - join.href = EV.meetUrl || '#'; join.target = '_blank'; join.rel = 'noopener'; - join.textContent = 'Join Google Meet โ†’'; - join.style.cssText = 'display:block;text-align:center;text-decoration:none;background:linear-gradient(180deg,#e9c249,#d4af37);' + - 'color:#0a1119;font-size:18px;font-weight:800;padding:15px;border-radius:12px;box-shadow:0 8px 22px rgba(212,175,55,.35);'; - join.addEventListener('click', done); - foot.appendChild(join); - - var st = EV.startUTC ? Date.parse(EV.startUTC) : 0; - if (st) { - var en = st + (Number(EV.durationMin) || 60) * 60000; - var f = function (ms) { return new Date(ms).toISOString().replace(/[-:]|\.\d{3}/g, ''); }; - var title = EV.eyebrow || 'Team RM Circle ยท Weekly Huddle'; - var details = (EV.meetUrl ? 'Join: ' + EV.meetUrl + '\n' : '') + String(EV.times || '').replace(/\|/g, ' '); - var gcal = 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=' + encodeURIComponent(title) + - '&dates=' + f(st) + '/' + f(en) + '&details=' + encodeURIComponent(details) + '&location=' + encodeURIComponent(EV.meetUrl || ''); - // On a phone a calendar.google.com link is claimed by the Google Calendar app, which opens its own - // home screen and throws the event details away. Phones get the calendar file instead: iOS, Android - // and the Google Calendar app all import it straight into the default calendar. - var phone = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || ''); - var row = document.createElement('div'); - row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px;'; - var btnCss = 'display:block;text-align:center;text-decoration:none;background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.35);' + - 'color:#f3c34a;font-size:13px;font-weight:700;padding:10px 8px;border-radius:10px;'; - var g = document.createElement('a'); - g.textContent = '๐Ÿ“… Add to Calendar'; g.style.cssText = btnCss; - if (phone) g.href = '/announce.ics?open=1'; - else { g.href = gcal; g.target = '_blank'; g.rel = 'noopener'; } - g.addEventListener('click', done); - var a = document.createElement('a'); - a.style.cssText = btnCss; - if (phone) { a.href = gcal; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Google Calendar'; } - else { a.href = '/announce.ics'; a.setAttribute('download', 'rm-circle-huddle.ics'); a.textContent = 'Apple / Outlook'; } - a.addEventListener('click', done); - row.appendChild(g); row.appendChild(a); foot.appendChild(row); - } - - if (EV.meetUrl) { - var link = document.createElement('div'); - link.textContent = EV.meetUrl.replace(/^https?:\/\//, ''); - link.style.cssText = 'text-align:center;color:#9fb0c2;font-size:13px;margin-top:10px;font-family:ui-monospace,Menlo,Consolas,monospace;word-break:break-all;'; - foot.appendChild(link); - } - - var later = document.createElement('button'); - later.textContent = 'Maybe later'; - later.style.cssText = 'display:block;margin:12px auto 0;background:none;border:none;color:#7d8ea0;font-size:13px;cursor:pointer;text-decoration:underline;'; - later.addEventListener('click', dismiss); - foot.appendChild(later); - - card.appendChild(x); card.appendChild(eyebrow); card.appendChild(img); card.appendChild(foot); - back.appendChild(card); document.body.appendChild(back); - document.addEventListener('keydown', onKey); - requestAnimationFrame(function () { back.style.opacity = '1'; }); - } - - setTimeout(build, 1100); - return true; - } - - /* Promo overlay: one flyer, one button, once per browser SESSION (sessionStorage), only on the - pages listed in promo.pages (path segment after the first slash: 'my', 'app', or 'all'). */ - function startPromo(P) { - if (!P || !P.enabled || !P.id || !P.img) return; - if (P.expiresUTC) { var exp = Date.parse(P.expiresUTC); if (exp && Date.now() > exp) return; } - var seg = (location.pathname.split('/')[1] || 'home').toLowerCase(); - var pages = String(P.pages || 'all').split(',').map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean); - if (pages.indexOf('all') < 0 && pages.indexOf(seg) < 0) return; - var KEY = 'rmc-promo-' + P.id; - try { if (sessionStorage.getItem(KEY) === 'seen') return; } catch (e) {} - var back; - function mark() { try { sessionStorage.setItem(KEY, 'seen'); } catch (e) {} } - function close() { if (!back) return; mark(); back.style.opacity = '0'; setTimeout(function () { if (back && back.parentNode) back.parentNode.removeChild(back); back = null; }, 260); document.removeEventListener('keydown', onKey); } - function onKey(e) { if (e.key === 'Escape') close(); } - function build() { - back = document.createElement('div'); - back.setAttribute('role', 'dialog'); back.setAttribute('aria-label', P.eyebrow || 'Upgrade'); - back.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(3,7,14,.84);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);opacity:0;transition:opacity .28s ease;overflow:auto;'; - back.addEventListener('click', function (e) { if (e.target === back) close(); }); - var card = document.createElement('div'); - card.style.cssText = 'position:relative;width:100%;max-width:420px;max-height:96vh;overflow:auto;border-radius:20px;background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7);font-family:system-ui,Segoe UI,Arial,sans-serif;'; - var x = document.createElement('button'); - x.setAttribute('aria-label', 'Close'); x.innerHTML = '×'; - x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:38px;height:38px;border-radius:50%;border:none;cursor:pointer;background:rgba(6,10,16,.72);color:#fff;font-size:24px;line-height:1;font-weight:700;display:grid;place-items:center;box-shadow:0 2px 10px rgba(0,0,0,.5);'; - x.addEventListener('click', close); - var img = document.createElement('img'); - img.src = P.img; img.alt = P.eyebrow || 'Upgrade'; img.style.cssText = 'display:block;width:100%;height:auto;border-radius:20px 20px 0 0;'; - var foot = document.createElement('div'); foot.style.cssText = 'padding:14px 18px 18px;'; - var cta = document.createElement('a'); - cta.href = P.ctaUrl || '/my'; cta.textContent = P.ctaText || 'See my next step \u2192'; - cta.style.cssText = 'display:block;text-align:center;text-decoration:none;background:linear-gradient(180deg,#e9c249,#d4af37);color:#0a1119;font-size:18px;font-weight:800;padding:15px;border-radius:12px;box-shadow:0 8px 22px rgba(212,175,55,.35);'; - cta.addEventListener('click', function (e) { - // already on the target page: close and scroll to the plan card instead of reloading - var here = (P.ctaUrl || '/my').split('#')[0].replace(/\/$/, '') === location.pathname.replace(/\/$/, ''); - if (here) { e.preventDefault(); close(); var t = document.querySelector('.ns-card, #nextStep, #plan'); if (t) setTimeout(function () { t.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 300); } - else mark(); - }); - var later = document.createElement('button'); - later.textContent = 'Not now'; later.style.cssText = 'display:block;margin:10px auto 0;background:none;border:none;color:#7d8ea0;font-size:13px;cursor:pointer;text-decoration:underline;'; - later.addEventListener('click', close); - foot.appendChild(cta); foot.appendChild(later); - card.appendChild(x); card.appendChild(img); card.appendChild(foot); - back.appendChild(card); document.body.appendChild(back); - document.addEventListener('keydown', onKey); - requestAnimationFrame(function () { back.style.opacity = '1'; }); - } - setTimeout(build, 900); - } - - function fetchAndGo() { - fetch('/api/announce', { headers: { 'Accept': 'application/json' } }) - .then(function (r) { return r.ok ? r.json() : null; }) - .then(function (EV) { if (!EV) return; var shown = start(EV); if (!shown) startPromo(EV.promo); }) - .catch(function () {}); - } - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fetchAndGo); - else fetchAndGo(); -})(); +/* announce.js โ€” dismissible event pop-up overlay (RM Circle). + Content is admin-controlled: fetched from /api/announce (edit it under + Admin โ†’ Settings โ†’ Event pop-up; no deploy needed). Self-gating: shows once + per browser until dismissed, and never after `expiresUTC`. */ +(function () { + function parseTimes(s) { + return String(s || '').split(/\r?\n+/).map(function (l) { return l.trim(); }).filter(Boolean) + .map(function (l) { var i = l.indexOf('|'); return i < 0 ? [l, ''] : [l.slice(0, i).trim(), l.slice(i + 1).trim()]; }); + } + + function start(EV) { + // A cancellation has no flyer and no meet link, so neither is required any more: the + // same card doubles as a plain notice (Marty, 2026-09-24). + if (!EV || !EV.enabled || !EV.id) return false; + if (EV.expiresUTC) { var exp = Date.parse(EV.expiresUTC); if (exp && Date.now() > exp) return false; } + var KEY = 'rmc-announce-' + EV.id; + try { if (localStorage.getItem(KEY) === 'done') return false; } catch (e) {} + EV.timesArr = parseTimes(EV.times); + + var back, card; + function done() { try { localStorage.setItem(KEY, 'done'); } catch (e) {} } + function close() { + if (!back) return; + back.style.opacity = '0'; + setTimeout(function () { if (back && back.parentNode) back.parentNode.removeChild(back); back = null; }, 260); + document.removeEventListener('keydown', onKey); + } + function dismiss() { done(); close(); } + function onKey(e) { if (e.key === 'Escape') dismiss(); } + + function build() { + back = document.createElement('div'); + back.setAttribute('role', 'dialog'); + back.setAttribute('aria-label', (EV.eyebrow || 'Event') + ' invitation'); + back.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;' + + 'padding:20px;background:rgba(3,7,14,.82);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);' + + 'opacity:0;transition:opacity .28s ease;overflow:auto;'; + back.addEventListener('click', function (e) { if (e.target === back) dismiss(); }); + + card = document.createElement('div'); + card.style.cssText = 'position:relative;width:100%;max-width:428px;max-height:94vh;overflow:auto;border-radius:20px;' + + 'background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7),0 0 0 1px rgba(212,175,55,.15);' + + 'font-family:system-ui,Segoe UI,Arial,sans-serif;'; + + var x = document.createElement('button'); + x.setAttribute('aria-label', 'Close'); + x.innerHTML = '×'; + x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:38px;height:38px;border-radius:50%;border:none;cursor:pointer;' + + 'background:rgba(6,10,16,.72);color:#fff;font-size:24px;line-height:1;font-weight:700;display:grid;place-items:center;box-shadow:0 2px 10px rgba(0,0,0,.5);'; + x.addEventListener('click', dismiss); + + var eyebrow = document.createElement('div'); + eyebrow.textContent = EV.eyebrow || ''; + eyebrow.style.cssText = 'text-align:center;letter-spacing:2px;text-transform:uppercase;font-size:11px;font-weight:700;color:#d4af37;padding:14px 44px 10px;'; + + var img = EV.img ? document.createElement('img') : null; + if (img) { + img.src = EV.img; + img.alt = EV.eyebrow || 'Event flyer'; + img.style.cssText = 'display:block;width:100%;height:auto;'; + } + + var foot = document.createElement('div'); + foot.style.cssText = 'padding:16px 20px 20px;'; + + if (EV.dateLabel) { + var dateRow = document.createElement('div'); + dateRow.innerHTML = '๐Ÿ—“ '; + dateRow.querySelector('b').textContent = EV.dateLabel; + dateRow.style.cssText = 'text-align:center;color:#eef2f7;font-size:15px;margin-bottom:12px;'; + foot.appendChild(dateRow); + } + + if (EV.timesArr.length) { + var grid = document.createElement('div'); + grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;'; + EV.timesArr.forEach(function (t) { + var cell = document.createElement('div'); + cell.style.cssText = 'background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.22);border-radius:10px;padding:8px 10px;'; + var time = document.createElement('div'); + time.style.cssText = 'font-size:15px;font-weight:800;color:#f3c34a;'; time.textContent = t[0]; + var region = document.createElement('div'); + region.style.cssText = 'font-size:11px;color:#9fb0c2;margin-top:1px;'; region.textContent = t[1] || 'ย '; + cell.appendChild(time); cell.appendChild(region); grid.appendChild(cell); + }); + foot.appendChild(grid); + } + + var join = EV.meetUrl ? document.createElement('a') : null; + if (join) { + join.href = EV.meetUrl; join.target = '_blank'; join.rel = 'noopener'; + join.textContent = 'Join Google Meet โ†’'; + join.style.cssText = 'display:block;text-align:center;text-decoration:none;background:linear-gradient(180deg,#e9c249,#d4af37);' + + 'color:#0a1119;font-size:18px;font-weight:800;padding:15px;border-radius:12px;box-shadow:0 8px 22px rgba(212,175,55,.35);'; + join.addEventListener('click', done); + foot.appendChild(join); + } + + var st = (EV.meetUrl && EV.startUTC) ? Date.parse(EV.startUTC) : 0; + if (st) { + var en = st + (Number(EV.durationMin) || 60) * 60000; + var f = function (ms) { return new Date(ms).toISOString().replace(/[-:]|\.\d{3}/g, ''); }; + var title = EV.eyebrow || 'Team RM Circle ยท Weekly Huddle'; + var details = (EV.meetUrl ? 'Join: ' + EV.meetUrl + '\n' : '') + String(EV.times || '').replace(/\|/g, ' '); + var gcal = 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=' + encodeURIComponent(title) + + '&dates=' + f(st) + '/' + f(en) + '&details=' + encodeURIComponent(details) + '&location=' + encodeURIComponent(EV.meetUrl || ''); + // On a phone a calendar.google.com link is claimed by the Google Calendar app, which opens its own + // home screen and throws the event details away. Phones get the calendar file instead: iOS, Android + // and the Google Calendar app all import it straight into the default calendar. + var phone = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || ''); + var row = document.createElement('div'); + row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px;'; + var btnCss = 'display:block;text-align:center;text-decoration:none;background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.35);' + + 'color:#f3c34a;font-size:13px;font-weight:700;padding:10px 8px;border-radius:10px;'; + var g = document.createElement('a'); + g.textContent = '๐Ÿ“… Add to Calendar'; g.style.cssText = btnCss; + if (phone) g.href = '/announce.ics?open=1'; + else { g.href = gcal; g.target = '_blank'; g.rel = 'noopener'; } + g.addEventListener('click', done); + var a = document.createElement('a'); + a.style.cssText = btnCss; + if (phone) { a.href = gcal; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Google Calendar'; } + else { a.href = '/announce.ics'; a.setAttribute('download', 'rm-circle-huddle.ics'); a.textContent = 'Apple / Outlook'; } + a.addEventListener('click', done); + row.appendChild(g); row.appendChild(a); foot.appendChild(row); + } + + if (EV.meetUrl) { + var link = document.createElement('div'); + link.textContent = EV.meetUrl.replace(/^https?:\/\//, ''); + link.style.cssText = 'text-align:center;color:#9fb0c2;font-size:13px;margin-top:10px;font-family:ui-monospace,Menlo,Consolas,monospace;word-break:break-all;'; + foot.appendChild(link); + } + + var later = document.createElement('button'); + later.textContent = 'Maybe later'; + later.style.cssText = 'display:block;margin:12px auto 0;background:none;border:none;color:#7d8ea0;font-size:13px;cursor:pointer;text-decoration:underline;'; + later.addEventListener('click', dismiss); + foot.appendChild(later); + + card.appendChild(x); card.appendChild(eyebrow); if (img) card.appendChild(img); card.appendChild(foot); + back.appendChild(card); document.body.appendChild(back); + document.addEventListener('keydown', onKey); + requestAnimationFrame(function () { back.style.opacity = '1'; }); + } + + setTimeout(build, 1100); + return true; + } + + /* Promo overlay: one flyer, one button, once per browser SESSION (sessionStorage), only on the + pages listed in promo.pages (path segment after the first slash: 'my', 'app', or 'all'). */ + function startPromo(P) { + if (!P || !P.enabled || !P.id || !P.img) return; + if (P.expiresUTC) { var exp = Date.parse(P.expiresUTC); if (exp && Date.now() > exp) return; } + var seg = (location.pathname.split('/')[1] || 'home').toLowerCase(); + var pages = String(P.pages || 'all').split(',').map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean); + if (pages.indexOf('all') < 0 && pages.indexOf(seg) < 0) return; + var KEY = 'rmc-promo-' + P.id; + try { if (sessionStorage.getItem(KEY) === 'seen') return; } catch (e) {} + var back; + function mark() { try { sessionStorage.setItem(KEY, 'seen'); } catch (e) {} } + function close() { if (!back) return; mark(); back.style.opacity = '0'; setTimeout(function () { if (back && back.parentNode) back.parentNode.removeChild(back); back = null; }, 260); document.removeEventListener('keydown', onKey); } + function onKey(e) { if (e.key === 'Escape') close(); } + function build() { + back = document.createElement('div'); + back.setAttribute('role', 'dialog'); back.setAttribute('aria-label', P.eyebrow || 'Upgrade'); + back.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(3,7,14,.84);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);opacity:0;transition:opacity .28s ease;overflow:auto;'; + back.addEventListener('click', function (e) { if (e.target === back) close(); }); + var card = document.createElement('div'); + card.style.cssText = 'position:relative;width:100%;max-width:420px;max-height:96vh;overflow:auto;border-radius:20px;background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7);font-family:system-ui,Segoe UI,Arial,sans-serif;'; + var x = document.createElement('button'); + x.setAttribute('aria-label', 'Close'); x.innerHTML = '×'; + x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:38px;height:38px;border-radius:50%;border:none;cursor:pointer;background:rgba(6,10,16,.72);color:#fff;font-size:24px;line-height:1;font-weight:700;display:grid;place-items:center;box-shadow:0 2px 10px rgba(0,0,0,.5);'; + x.addEventListener('click', close); + var img = document.createElement('img'); + img.src = P.img; img.alt = P.eyebrow || 'Upgrade'; img.style.cssText = 'display:block;width:100%;height:auto;border-radius:20px 20px 0 0;'; + var foot = document.createElement('div'); foot.style.cssText = 'padding:14px 18px 18px;'; + var cta = document.createElement('a'); + cta.href = P.ctaUrl || '/my'; cta.textContent = P.ctaText || 'See my next step \u2192'; + cta.style.cssText = 'display:block;text-align:center;text-decoration:none;background:linear-gradient(180deg,#e9c249,#d4af37);color:#0a1119;font-size:18px;font-weight:800;padding:15px;border-radius:12px;box-shadow:0 8px 22px rgba(212,175,55,.35);'; + cta.addEventListener('click', function (e) { + // already on the target page: close and scroll to the plan card instead of reloading + var here = (P.ctaUrl || '/my').split('#')[0].replace(/\/$/, '') === location.pathname.replace(/\/$/, ''); + if (here) { e.preventDefault(); close(); var t = document.querySelector('.ns-card, #nextStep, #plan'); if (t) setTimeout(function () { t.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 300); } + else mark(); + }); + var later = document.createElement('button'); + later.textContent = 'Not now'; later.style.cssText = 'display:block;margin:10px auto 0;background:none;border:none;color:#7d8ea0;font-size:13px;cursor:pointer;text-decoration:underline;'; + later.addEventListener('click', close); + foot.appendChild(cta); foot.appendChild(later); + card.appendChild(x); if (img) card.appendChild(img); card.appendChild(foot); + back.appendChild(card); document.body.appendChild(back); + document.addEventListener('keydown', onKey); + requestAnimationFrame(function () { back.style.opacity = '1'; }); + } + setTimeout(build, 900); + } + + function fetchAndGo() { + fetch('/api/announce', { headers: { 'Accept': 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (EV) { if (!EV) return; var shown = start(EV); if (!shown) startPromo(EV.promo); }) + .catch(function () {}); + } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fetchAndGo); + else fetchAndGo(); +})();