Event pop-up: Add-to-calendar buttons (Google Calendar link + .ics for Apple/Outlook)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -46,7 +46,7 @@ function render(){
|
|||||||
if(!grantAutoLoaded){grantAutoLoaded=true;if(window.agBoot)window.agBoot();}
|
if(!grantAutoLoaded){grantAutoLoaded=true;if(window.agBoot)window.agBoot();}
|
||||||
if(!msgsAutoLoaded){msgsAutoLoaded=true;loadAdminMsgs();}
|
if(!msgsAutoLoaded){msgsAutoLoaded=true;loadAdminMsgs();}
|
||||||
const efi=document.getElementById('emailFromInput');if(efi&&!efi.value)efi.value=state.config.emailFrom||em.from||'';
|
const efi=document.getElementById('emailFromInput');if(efi&&!efi.value)efi.value=state.config.emailFrom||em.from||'';
|
||||||
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','teamAlertEmail','ownerAlertEmail','directDefaultIds','announceEyebrow','announceImg','announceMeetUrl','announceDateLabel','announceTimes','announceExpiresUTC','announceId'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;if(f.elements.announceEnabled)f.elements.announceEnabled.checked=c.announceEnabled===undefined?true:!!c.announceEnabled;
|
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','teamAlertEmail','ownerAlertEmail','directDefaultIds','announceEyebrow','announceImg','announceMeetUrl','announceDateLabel','announceTimes','announceExpiresUTC','announceStartUTC','announceDurationMin','announceId'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;if(f.elements.announceEnabled)f.elements.announceEnabled.checked=c.announceEnabled===undefined?true:!!c.announceEnabled;
|
||||||
}
|
}
|
||||||
document.getElementById('loginForm').addEventListener('submit',async e=>{e.preventDefault();const err=document.getElementById('loginError');err.textContent='';try{await api('/api/admin/login',{method:'POST',body:JSON.stringify({password:document.getElementById('password').value})});document.getElementById('password').value='';await loadState()}catch(x){err.textContent=x.message}});
|
document.getElementById('loginForm').addEventListener('submit',async e=>{e.preventDefault();const err=document.getElementById('loginError');err.textContent='';try{await api('/api/admin/login',{method:'POST',body:JSON.stringify({password:document.getElementById('password').value})});document.getElementById('password').value='';await loadState()}catch(x){err.textContent=x.message}});
|
||||||
document.getElementById('logoutBtn').addEventListener('click',async()=>{await api('/api/admin/logout',{method:'POST'});location.reload()});
|
document.getElementById('logoutBtn').addEventListener('click',async()=>{await api('/api/admin/logout',{method:'POST'});location.reload()});
|
||||||
|
|||||||
@@ -90,6 +90,27 @@
|
|||||||
join.addEventListener('click', done);
|
join.addEventListener('click', done);
|
||||||
foot.appendChild(join);
|
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 || '');
|
||||||
|
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.href = gcal; g.target = '_blank'; g.rel = 'noopener'; g.textContent = '📅 Google Calendar'; g.style.cssText = btnCss;
|
||||||
|
g.addEventListener('click', done);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = '/announce.ics'; a.setAttribute('download', 'rm-circle-huddle.ics'); a.textContent = '📅 Apple / Outlook'; a.style.cssText = btnCss;
|
||||||
|
a.addEventListener('click', done);
|
||||||
|
row.appendChild(g); row.appendChild(a); foot.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
if (EV.meetUrl) {
|
if (EV.meetUrl) {
|
||||||
var link = document.createElement('div');
|
var link = document.createElement('div');
|
||||||
link.textContent = EV.meetUrl.replace(/^https?:\/\//, '');
|
link.textContent = EV.meetUrl.replace(/^https?:\/\//, '');
|
||||||
|
|||||||
@@ -420,7 +420,9 @@ const ANNOUNCE_DEFAULTS = {
|
|||||||
announceEyebrow: 'Team RM Circle · Weekly Huddle',
|
announceEyebrow: 'Team RM Circle · Weekly Huddle',
|
||||||
announceDateLabel: 'Tuesday, September 8',
|
announceDateLabel: 'Tuesday, September 8',
|
||||||
announceTimes: '7:00 PM CST | USA / Canada\n8:00 PM EST |\n8:00 PM AST | Caribbean\n1:00 AM | UK',
|
announceTimes: '7:00 PM CST | USA / Canada\n8:00 PM EST |\n8:00 PM AST | Caribbean\n1:00 AM | UK',
|
||||||
announceExpiresUTC: '2026-09-09T02:30:00Z'
|
announceExpiresUTC: '2026-09-09T02:30:00Z',
|
||||||
|
announceStartUTC: '',
|
||||||
|
announceDurationMin: 60
|
||||||
};
|
};
|
||||||
function seedAnnounceDefaults() {
|
function seedAnnounceDefaults() {
|
||||||
try { const c = readJson(CONFIG_FILE) || {}; let ch = false;
|
try { const c = readJson(CONFIG_FILE) || {}; let ch = false;
|
||||||
@@ -582,9 +584,27 @@ async function handleApi(req,res,pathname){
|
|||||||
enabled: c.announceEnabled===undefined ? true : !!c.announceEnabled,
|
enabled: c.announceEnabled===undefined ? true : !!c.announceEnabled,
|
||||||
id: c.announceId||'', img: c.announceImg||'', meetUrl: c.announceMeetUrl||'',
|
id: c.announceId||'', img: c.announceImg||'', meetUrl: c.announceMeetUrl||'',
|
||||||
eyebrow: c.announceEyebrow||'', dateLabel: c.announceDateLabel||'',
|
eyebrow: c.announceEyebrow||'', dateLabel: c.announceDateLabel||'',
|
||||||
times: c.announceTimes||'', expiresUTC: c.announceExpiresUTC||''
|
times: c.announceTimes||'', expiresUTC: c.announceExpiresUTC||'',
|
||||||
|
startUTC: c.announceStartUTC||'', durationMin: Number(c.announceDurationMin)||60
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// calendar file for the event pop-up (Apple Calendar / Outlook); Google uses a render URL built client-side
|
||||||
|
if(req.method==='GET'&&pathname==='/announce.ics'){
|
||||||
|
const c=getConfig();const st=Date.parse(c.announceStartUTC||'');
|
||||||
|
if(!st)return json(res,404,{error:'No event start set.'});
|
||||||
|
const dur=(Number(c.announceDurationMin)||60)*60000;
|
||||||
|
const fmt=d=>new Date(d).toISOString().replace(/[-:]|\.\d{3}/g,'');
|
||||||
|
const esc=v=>String(v||'').replace(/\\/g,'\\\\').replace(/;/g,'\\;').replace(/,/g,'\\,').replace(/\r?\n/g,'\\n');
|
||||||
|
const title=c.announceEyebrow||'Team RM Circle · Weekly Huddle';
|
||||||
|
const desc=(c.announceMeetUrl?'Join: '+c.announceMeetUrl+'\n':'')+String(c.announceTimes||'').replace(/\|/g,' ');
|
||||||
|
const ics=['BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//RM Circle//Event//EN','CALSCALE:GREGORIAN','METHOD:PUBLISH','BEGIN:VEVENT',
|
||||||
|
'UID:'+(c.announceId||'event')+'@rmcircle.team','DTSTAMP:'+fmt(Date.now()),'DTSTART:'+fmt(st),'DTEND:'+fmt(st+dur),
|
||||||
|
'SUMMARY:'+esc(title),'DESCRIPTION:'+esc(desc),'LOCATION:'+esc(c.announceMeetUrl||''),
|
||||||
|
...(c.announceMeetUrl?['URL:'+c.announceMeetUrl]:[]),
|
||||||
|
'BEGIN:VALARM','TRIGGER:-PT30M','ACTION:DISPLAY','DESCRIPTION:'+esc(title),'END:VALARM','END:VEVENT','END:VCALENDAR'].join('\r\n')+'\r\n';
|
||||||
|
res.writeHead(200,{'Content-Type':'text/calendar; charset=utf-8','Content-Disposition':'attachment; filename="rm-circle-huddle.ics"','Cache-Control':'no-store'});
|
||||||
|
return res.end(ics);
|
||||||
|
}
|
||||||
if(req.method==='GET'&&pathname==='/api/public/member'){
|
if(req.method==='GET'&&pathname==='/api/public/member'){
|
||||||
const ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
|
const ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
|
||||||
if(memberLookupLimited(ip))return json(res,429,{error:'Too many lookups — give it a minute.'});
|
if(memberLookupLimited(ip))return json(res,429,{error:'Too many lookups — give it a minute.'});
|
||||||
@@ -1580,7 +1600,7 @@ async function handleApi(req,res,pathname){
|
|||||||
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
|
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
|
||||||
}
|
}
|
||||||
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
|
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
|
||||||
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','telegramProofChatId','telegramProofTopicId','telegramProofEvents','telegramProofCtaUrl','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts','suiteLevelOverride','directDefaultIds','announceEnabled','announceId','announceImg','announceMeetUrl','announceEyebrow','announceDateLabel','announceTimes','announceExpiresUTC'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;if('announceEnabled' in next)next.announceEnabled=!!next.announceEnabled;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','telegramProofChatId','telegramProofTopicId','telegramProofEvents','telegramProofCtaUrl','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts','suiteLevelOverride','directDefaultIds','announceEnabled','announceId','announceImg','announceMeetUrl','announceEyebrow','announceDateLabel','announceTimes','announceExpiresUTC','announceStartUTC','announceDurationMin'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];if('announceDurationMin' in next)next.announceDurationMin=Math.max(15,Number(next.announceDurationMin)||60);next.premiumEntryPol=Number(next.premiumEntryPol)||362;if('announceEnabled' in next)next.announceEnabled=!!next.announceEnabled;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
||||||
}
|
}
|
||||||
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
|
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
|
||||||
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
|
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
|
||||||
|
|||||||
Reference in New Issue
Block a user