Event Detail page
This commit is contained in:
@@ -21,6 +21,8 @@ export type MeetingItem = {
|
||||
venue: string;
|
||||
landmark: string;
|
||||
poster: string;
|
||||
/** Long description shown on the meeting's own page. */
|
||||
details: string;
|
||||
};
|
||||
|
||||
export type PastEvent = {
|
||||
@@ -29,6 +31,8 @@ export type PastEvent = {
|
||||
date: string;
|
||||
title: string;
|
||||
text: string;
|
||||
/** Long description shown on the event's own page. */
|
||||
details: string;
|
||||
};
|
||||
|
||||
export type SiteContent = {
|
||||
@@ -105,6 +109,7 @@ export function sanitizeContent(input: unknown): SiteContent {
|
||||
venue: str(x.venue),
|
||||
landmark: str(x.landmark),
|
||||
poster: img(x.poster),
|
||||
details: str(x.details, 10000),
|
||||
}))
|
||||
.filter((x) => x.date)
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
@@ -116,6 +121,7 @@ export function sanitizeContent(input: unknown): SiteContent {
|
||||
date: str(e.date, 60),
|
||||
title: str(e.title, 150),
|
||||
text: str(e.text, 600),
|
||||
details: str(e.details, 10000),
|
||||
}))
|
||||
.filter((e) => e.title);
|
||||
return {
|
||||
|
||||
@@ -22,3 +22,75 @@ export function formatMeetingDate(date: string) {
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
|
||||
/** A card in the website's "Past Events" list: a manual event or a completed meeting. */
|
||||
export type PastEventCard = {
|
||||
id: string;
|
||||
href: string;
|
||||
image: string;
|
||||
date: string;
|
||||
title: string;
|
||||
text: string;
|
||||
kind: "event" | "meeting";
|
||||
};
|
||||
|
||||
/** Best-effort sortable timestamp from free-text dates like "12 Aug 2026" or "Aug 2026". */
|
||||
function toTime(date: string): number | null {
|
||||
if (!date) return null;
|
||||
const t = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(date) ? `${date}T00:00:00Z` : date);
|
||||
return Number.isNaN(t) ? null : t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Website "Past Events" = events added by the admin + meetings whose date has passed.
|
||||
* Newest first. Admin events without a readable date keep their admin order at the top.
|
||||
*/
|
||||
export function buildPastEvents(
|
||||
events: { id: string; image: string; date: string; title: string; text: string }[],
|
||||
meetings: (MeetingItem & { details?: string })[],
|
||||
): PastEventCard[] {
|
||||
const today = todayIST();
|
||||
const cards: (PastEventCard & { t: number | null; i: number })[] = [
|
||||
...events.map((e, i) => ({
|
||||
id: e.id,
|
||||
href: `/events/${e.id}`,
|
||||
image: e.image,
|
||||
date: e.date,
|
||||
title: e.title,
|
||||
text: e.text,
|
||||
kind: "event" as const,
|
||||
t: toTime(e.date),
|
||||
i,
|
||||
})),
|
||||
...meetings
|
||||
.filter((m) => m.date < today)
|
||||
.map((m, i) => ({
|
||||
id: `m-${m.id}`,
|
||||
href: `/meetings/${m.id}`,
|
||||
image: m.poster,
|
||||
date: formatMeetingDate(m.date),
|
||||
title: m.topic || "Club meeting",
|
||||
text:
|
||||
(m.details ?? "").replace(/\s+/g, " ").trim().slice(0, 160) ||
|
||||
[m.venue && `Club meeting at ${m.venue}`, m.time].filter(Boolean).join(" · "),
|
||||
kind: "meeting" as const,
|
||||
t: toTime(m.date),
|
||||
i: events.length + i,
|
||||
})),
|
||||
];
|
||||
cards.sort((a, b) => {
|
||||
if (a.t === null && b.t === null) return a.i - b.i;
|
||||
if (a.t === null) return -1;
|
||||
if (b.t === null) return 1;
|
||||
return b.t - a.t || a.i - b.i;
|
||||
});
|
||||
return cards.map((c) => ({
|
||||
id: c.id,
|
||||
href: c.href,
|
||||
image: c.image,
|
||||
date: c.date,
|
||||
title: c.title,
|
||||
text: c.text,
|
||||
kind: c.kind,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user