diff --git a/app/programs/ProgramsPageClient.tsx b/app/programs/ProgramsPageClient.tsx index a49a03e7b..d7bd3d658 100644 --- a/app/programs/ProgramsPageClient.tsx +++ b/app/programs/ProgramsPageClient.tsx @@ -13,7 +13,8 @@ import { BtnArrowSvg } from "../../components/landing/btn-arrow"; function ProgramCard({ program }: { program: AirtableProgram }) { const wrapperRef = useRef(null); const now = new Date(); - const isEnded = parseLocalDate(program.endDate) < now; + // If no end date, program runs indefinitely (never ends) + const isEnded = program.endDate ? parseLocalDate(program.endDate) < now : false; const isDraft = parseLocalDate(program.startDate) > now; const s = program.site; @@ -41,7 +42,9 @@ function ProgramCard({ program }: { program: AirtableProgram }) { ? "Coming soon" : isEnded ? "Ended" - : `Ends ${parseLocalDate(program.endDate).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}`; + : program.endDate + ? `Ends ${parseLocalDate(program.endDate).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}` + : "Ongoing"; const badgeEnded = isEnded || isDraft; // Italic metadata lines @@ -709,10 +712,13 @@ export default function ProgramsPage({ const bPinned = Number(Boolean(b.site?.pinned)); if (aPinned !== bPinned) return bPinned - aPinned; - if (sort === "deadline-asc") - return parseLocalDate(a.endDate).getTime() - parseLocalDate(b.endDate).getTime(); - if (sort === "deadline-desc") - return parseLocalDate(b.endDate).getTime() - parseLocalDate(a.endDate).getTime(); + // For deadline sorting, treat null endDate as far future (never ends) + const FAR_FUTURE = new Date(9999, 11, 31).getTime(); + const aEndTime = a.endDate ? parseLocalDate(a.endDate).getTime() : FAR_FUTURE; + const bEndTime = b.endDate ? parseLocalDate(b.endDate).getTime() : FAR_FUTURE; + + if (sort === "deadline-asc") return aEndTime - bEndTime; + if (sort === "deadline-desc") return bEndTime - aEndTime; if (sort === "az") return a.name.localeCompare(b.name); return b.name.localeCompare(a.name); }); diff --git a/app/programs/edit/page.tsx b/app/programs/edit/page.tsx index 07e7f3898..568001e97 100644 --- a/app/programs/edit/page.tsx +++ b/app/programs/edit/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { Navbar } from "../../../components/Navbar"; -import type { AirtableProgram } from "../../../lib/programs"; +import { parseLocalDate, type AirtableProgram } from "../../../lib/programs"; import type { SiteProgram, ProjectType, ProgramFormat } from "../../../lib/site-programs"; import { PROJECT_TYPE_OPTIONS, formatInPersonDate } from "../../../lib/site-programs"; import { BtnArrowSvg } from "../../../components/landing/btn-arrow"; @@ -38,17 +38,16 @@ function CardPreview({ prog }: { prog: EditorProgram }) { const logoUrl = site?.logoUrl ?? null; const bgImageUrl = draft.bgType === "image" ? (site?.bgImageUrl ?? null) : null; const now = new Date(); - function parseLocalDate(iso: string) { - const [y, m, d] = iso.split("-").map(Number); - return new Date(y, m - 1, d); - } - const isEnded = parseLocalDate(ysws.endDate) < now; + // If no end date, program runs indefinitely (never ends) + const isEnded = ysws.endDate ? parseLocalDate(ysws.endDate) < now : false; const isDraft = parseLocalDate(ysws.startDate) > now; const badgeLabel = isDraft ? "Coming soon" : isEnded ? "Ended" - : `Ends ${parseLocalDate(ysws.endDate).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}`; + : ysws.endDate + ? `Ends ${parseLocalDate(ysws.endDate).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}` + : "Ongoing"; const badgeEnded = isEnded || isDraft; const buttonText = isEnded ? "See the site" : "Start now"; const buttonColor = draft.buttonColor || "#ec3750"; @@ -1716,17 +1715,19 @@ export default function EditPage() { color: "var(--muted)", }} > - {new Date(prog.ysws.startDate).toLocaleDateString("en-GB", { + {parseLocalDate(prog.ysws.startDate).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", })}{" "} –{" "} - {new Date(prog.ysws.endDate).toLocaleDateString("en-GB", { - day: "numeric", - month: "short", - year: "numeric", - })} + {prog.ysws.endDate + ? parseLocalDate(prog.ysws.endDate).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }) + : "Ongoing"} {expanded === prog.ysws.name ? "▲" : "▼"} diff --git a/components/landing/events.tsx b/components/landing/events.tsx index f8eb17777..d5fac72dd 100644 --- a/components/landing/events.tsx +++ b/components/landing/events.tsx @@ -25,7 +25,8 @@ function SkeletonCard() { // ── Dynamic event card ─────────────────────────────────────────────────────── function EventCard({ program }: { program: AirtableProgram }) { const wrapperRef = useRef(null); - const endDate = parseLocalDate(program.endDate); + // endDate may be null for indefinite programs + const endDate = program.endDate ? parseLocalDate(program.endDate) : null; const s = program.site; const bgColor = s?.bgColor ?? "var(--surface)"; @@ -60,8 +61,10 @@ function EventCard({ program }: { program: AirtableProgram }) { } else { badgeLabel = `${start.getDate()} ${start.toLocaleDateString("en-GB", { month: "short" })} – ${end.getDate()} ${end.toLocaleDateString("en-GB", { month: "short" })} ${end.getFullYear()}`; } - } else { + } else if (endDate) { badgeLabel = `Ends ${endDate.toLocaleDateString("en-GB", { day: "numeric", month: "short" })}`; + } else { + badgeLabel = "Ongoing"; } return ( diff --git a/lib/programs-data.ts b/lib/programs-data.ts index c3ea79f27..8e8414b96 100644 --- a/lib/programs-data.ts +++ b/lib/programs-data.ts @@ -26,7 +26,7 @@ async function readPrograms({ fresh = false }: FetchProgramsOptions = {}): Promi } const params = new URLSearchParams(); - params.set("filterByFormula", "AND(NOT({Start Date}=''), NOT({End Date}=''))"); + params.set("filterByFormula", "NOT({Start Date}='')"); params.append("fields[]", "Name"); params.append("fields[]", "Start Date"); params.append("fields[]", "End Date"); @@ -73,7 +73,7 @@ async function readPrograms({ fresh = false }: FetchProgramsOptions = {}): Promi id: record.id, name, startDate: record.fields["Start Date"], - endDate: record.fields["End Date"], + endDate: record.fields["End Date"] || null, websiteUrl: record.fields["Website URL"] ?? null, site: siteMap.get(name) ?? null, }; diff --git a/lib/programs.ts b/lib/programs.ts index 439fdfcb3..095faa97f 100644 --- a/lib/programs.ts +++ b/lib/programs.ts @@ -4,7 +4,7 @@ export interface AirtableProgram { id: string; name: string; startDate: string; - endDate: string; + endDate: string | null; websiteUrl: string | null; site: SiteProgram | null; } @@ -21,7 +21,8 @@ export function getProgramStatus( now = new Date(), ): ProgramStatus { const started = parseLocalDate(program.startDate) <= now; - const ended = parseLocalDate(program.endDate) < now; + // If no end date, program runs indefinitely (never ends) + const ended = program.endDate ? parseLocalDate(program.endDate) < now : false; return !started ? "draft" : ended ? "ended" : "ongoing"; }