102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
// Date helpers shared by the site and the admin panel (no server-only imports).
|
|
import type { MeetingItem } from "./content";
|
|
|
|
/** Today's date in India as YYYY-MM-DD. */
|
|
export function todayIST() {
|
|
return new Date().toLocaleDateString("en-CA", { timeZone: "Asia/Kolkata" });
|
|
}
|
|
|
|
/** The next meeting on or after today, if any. */
|
|
export function getNextMeeting(list: MeetingItem[]): MeetingItem | undefined {
|
|
const today = todayIST();
|
|
return [...list].filter((m) => m.date >= today).sort((a, b) => a.date.localeCompare(b.date))[0];
|
|
}
|
|
|
|
export function formatMeetingDate(date: string) {
|
|
const [y, m, d] = date.split("-").map(Number);
|
|
return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString("en-IN", {
|
|
weekday: "short",
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
timeZone: "UTC",
|
|
});
|
|
}
|
|
|
|
/** Event dates are saved as YYYY-MM-DD (date picker); older free-text dates are shown as typed. */
|
|
export function formatEventDate(date: string) {
|
|
return /^\d{4}-\d{2}-\d{2}$/.test(date) ? formatMeetingDate(date) : date;
|
|
}
|
|
|
|
/** 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: formatEventDate(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,
|
|
}));
|
|
}
|