diff --git a/website/src/pages/webinars.module.css b/website/src/pages/webinars.module.css index 31fde8ece..7571ec9f5 100644 --- a/website/src/pages/webinars.module.css +++ b/website/src/pages/webinars.module.css @@ -140,6 +140,17 @@ color: #fff !important; } +.cancelledBadge { + display: inline-block; + font-size: 0.8rem; + font-weight: 600; + color: var(--ifm-color-warning-darkest, #7a5900); + background: var(--ifm-color-warning-lightest, #fff3cd); + border: 1px solid var(--ifm-color-warning-dark, #d4a017); + border-radius: 4px; + padding: 0.35rem 0.9rem; +} + @media (prefers-reduced-motion: reduce) { .joinBtn { transition: none; diff --git a/website/src/pages/webinars.tsx b/website/src/pages/webinars.tsx index 2d5646681..916aaf9db 100644 --- a/website/src/pages/webinars.tsx +++ b/website/src/pages/webinars.tsx @@ -35,6 +35,7 @@ interface MonthlyAgendaResult { month?: string; items: AgendaItem[]; loading: boolean; + cancelled?: boolean; } interface EventType { @@ -46,7 +47,7 @@ interface EventType { joinHref: string; agendaBasePath: string; agendaMode: "monthly" | "static"; - getNextDate: () => Date; + getNextDate: (after?: Date) => Date; pastRecordingsHref?: string; } @@ -85,14 +86,13 @@ const eventTypes: EventType[] = [ // --- Date Helpers ---------------------------------------------------------- // Returns the upcoming 3rd Wednesday (or today if today is the 3rd Wednesday). -function getNextThirdWednesday(): Date { +// Pass `after` to find the next occurrence on or after that date. +function getNextThirdWednesday(after?: Date): Date { const PST_OFFSET_MS = 8 * 60 * 60 * 1000; - const nowUTC = Date.now(); - const nowPST = new Date(nowUTC - PST_OFFSET_MS); - - const year = nowPST.getUTCFullYear(); - const month = nowPST.getUTCMonth(); - const today = nowPST.getUTCDate(); + const referencePST = new Date((after?.getTime() ?? Date.now()) - PST_OFFSET_MS); + const year = referencePST.getUTCFullYear(); + const month = referencePST.getUTCMonth(); + const day = referencePST.getUTCDate(); const thirdWed = (y: number, m: number): Date => { const first = new Date(Date.UTC(y, m, 1)); @@ -101,7 +101,7 @@ function getNextThirdWednesday(): Date { }; const candidate = thirdWed(year, month); - if (candidate.getUTCDate() >= today && candidate.getUTCMonth() === month) { + if (candidate.getUTCDate() >= day && candidate.getUTCMonth() === month) { return new Date(candidate.getUTCFullYear(), candidate.getUTCMonth(), candidate.getUTCDate()); } const nextMonth = month + 1; @@ -112,25 +112,24 @@ function getNextThirdWednesday(): Date { return new Date(result.getUTCFullYear(), result.getUTCMonth(), result.getUTCDate()); } -// Returns the next Architecture Review Hour date. +// Returns the next Kube & Tell date. // Known upcoming dates: June 11, 2026. After that, assume 2nd Thursday monthly. -function getNextKubeAndTellDate(): Date { +// Pass `after` to find the next occurrence on or after that date. +function getNextKubeAndTellDate(after?: Date): Date { const PST_OFFSET_MS = 8 * 60 * 60 * 1000; - const nowUTC = Date.now(); - const nowPST = new Date(nowUTC - PST_OFFSET_MS); - const todayMs = Date.UTC(nowPST.getUTCFullYear(), nowPST.getUTCMonth(), nowPST.getUTCDate()); + const referencePST = new Date((after?.getTime() ?? Date.now()) - PST_OFFSET_MS); + const year = referencePST.getUTCFullYear(); + const month = referencePST.getUTCMonth(); + const day = referencePST.getUTCDate(); + const referenceDayMs = Date.UTC(year, month, day); // Known fixed date const knownMs = Date.UTC(2026, 5, 11); // June 11, 2026 - if (knownMs >= todayMs) { + if (knownMs >= referenceDayMs) { return new Date(2026, 5, 11); } // Fallback: 2nd Thursday of each month - const year = nowPST.getUTCFullYear(); - const month = nowPST.getUTCMonth(); - const today = nowPST.getUTCDate(); - const secondThursday = (y: number, m: number): Date => { const first = new Date(Date.UTC(y, m, 1)); const offset = (4 - first.getUTCDay() + 7) % 7; @@ -138,7 +137,9 @@ function getNextKubeAndTellDate(): Date { }; const candidate = secondThursday(year, month); - if (candidate.getUTCDate() >= today && candidate.getUTCMonth() === month) { + const isCurrentMonthCandidate = candidate.getUTCMonth() === month; + const isUpcomingCandidate = candidate.getUTCDate() >= day; + if (isCurrentMonthCandidate && isUpcomingCandidate) { return new Date(candidate.getUTCFullYear(), candidate.getUTCMonth(), candidate.getUTCDate()); } const nextMonth = month + 1; @@ -162,8 +163,10 @@ function formatDate(d: Date): string { function parseAgendaMarkdown(raw: string): { month?: string; items: AgendaItem[]; + cancelled?: boolean; } { let month: string | undefined; + let cancelled: boolean | undefined; let content = raw.trim(); if (content.startsWith("---")) { const end = content.indexOf("\n---", 3); @@ -173,6 +176,8 @@ function parseAgendaMarkdown(raw: string): { front.split(/\r?\n/).forEach((line) => { const m = line.match(/^month:\s*(.+)$/i); if (m) month = m[1].trim(); + const c = line.match(/^cancelled:\s*(.+)$/i); + if (c) cancelled = /true/i.test(c[1].trim()); }); } } @@ -217,7 +222,7 @@ function parseAgendaMarkdown(raw: string): { if (featured) item.featured = true; return item; }); - return { month, items: finalized }; + return { month, items: finalized, cancelled }; } // Hook to load agenda markdown for current month, falling back to latest prior month. @@ -258,6 +263,7 @@ function useMonthlyAgenda(basePath: string): MonthlyAgendaResult { setState({ items: parsed.items, month: parsed.month, + cancelled: parsed.cancelled, loading: false, }); }) @@ -309,7 +315,7 @@ function useStaticAgenda(filePath: string): MonthlyAgendaResult { .then((text) => { if (cancelled) return; const parsed = parseAgendaMarkdown(text); - setState({ items: parsed.items, month: parsed.month, loading: false }); + setState({ items: parsed.items, month: parsed.month, cancelled: parsed.cancelled, loading: false }); }) .catch(() => { if (!cancelled) setState({ items: [], loading: false }); @@ -343,10 +349,14 @@ function Hero(): ReactNode { function EventSection({ event }: { event: EventType }): ReactNode { const monthlyResult = useMonthlyAgenda(event.agendaMode === "monthly" ? event.agendaBasePath : ""); const staticResult = useStaticAgenda(event.agendaMode === "static" ? event.agendaBasePath : ""); - const { month, items, loading } = event.agendaMode === "monthly" ? monthlyResult : staticResult; + const { month, items, loading, cancelled } = event.agendaMode === "monthly" ? monthlyResult : staticResult; const label = month || "Latest"; const empty = !loading && items.length === 0; - const nextDate = event.getNextDate(); + // When cancelled, advance to the next month's occurrence using the same date logic. + const rawNextDate = event.getNextDate(); + const nextDate = cancelled + ? event.getNextDate(new Date(rawNextDate.getFullYear(), rawNextDate.getMonth() + 1, 1)) + : rawNextDate; return (
@@ -372,29 +382,38 @@ function EventSection({ event }: { event: EventType }): ReactNode { Add to calendar - - Join Now - + {cancelled ? ( + No call this month + ) : ( + + Join Now + + )}
Agenda - {label} - {loading && ( + {!cancelled && loading && ( loading… )}
- {empty && ( + {cancelled && ( +

+ There is no {event.label} call this month. The next session is listed above. +

+ )} + {!cancelled && empty && (

No agenda has been published yet. Once an agenda file is added it appears here automatically.

)} - {!empty && ( + {!cancelled && !empty && (