2c6b116e9a
On a phone the Google Calendar app claims calendar.google.com links and opens its own home screen, dropping the event. The pop-up now offers one "Add to Calendar" button that goes to the calendar file on iOS/Android (served inline so the phone hands it to its calendar) and to the Google event editor on desktop, with the other route as the second button either way. The .ics is also folded to RFC 5545 line lengths so Outlook accepts it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
203 lines
13 KiB
JavaScript
203 lines
13 KiB
JavaScript
/* 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 = '🗓 <b></b>';
|
||
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();
|
||
})();
|