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:
@@ -420,7 +420,9 @@ const ANNOUNCE_DEFAULTS = {
|
||||
announceEyebrow: 'Team RM Circle · Weekly Huddle',
|
||||
announceDateLabel: 'Tuesday, September 8',
|
||||
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() {
|
||||
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,
|
||||
id: c.announceId||'', img: c.announceImg||'', meetUrl: c.announceMeetUrl||'',
|
||||
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'){
|
||||
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.'});
|
||||
@@ -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});
|
||||
}
|
||||
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))?$/);
|
||||
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