import fs from "fs/promises"; import path from "path"; import { parse } from "date-fns"; import matter from "gray-matter"; import { MDXRemoteSerializeResult } from "next-mdx-remote"; import { serialize } from "next-mdx-remote/serialize"; import { Term, TERMS, isTerm, DATE_FORMAT, getLocalDateFromEST, TermYear, getTermYear, getCurrentTermYear, } from "@/utils"; import type { Props } from "../pages/events/[year]/[term]"; const EVENTS_PATH = path.join("content", "events"); export async function getEventYears(): Promise { return (await fs.readdir(EVENTS_PATH, { withFileTypes: true })) .filter((dirent) => dirent.isDirectory()) .map((dirent) => parseInt(dirent.name)) .sort(); } export async function getEventTermsByYear(year: number): Promise { return ( await fs.readdir(path.join(EVENTS_PATH, year.toString()), { withFileTypes: true, }) ) .filter((dirent) => dirent.isDirectory() && isTerm(dirent.name)) .map((dirent) => dirent.name as Term) .sort((a, b) => TERMS.indexOf(a) - TERMS.indexOf(b)); } interface RawMetadata { name: string; poster?: string; short: string; startDate: string; endDate?: string; online?: boolean; location: string; registerLink?: string; } interface Metadata { name: string; poster?: string; short: string; startDate: string; endDate?: string; online: boolean; location: string; permaLink: string; registerLink?: string; year: number; term: string; slug: string; } export interface Event { content: MDXRemoteSerializeResult>; metadata: Metadata; } export async function getEventBySlug( year: number, term: Term, slug: string ): Promise { const file = await fs.readFile( path.join(EVENTS_PATH, year.toString(), term, `${slug}.md`), "utf-8" ); const { content, data } = matter(file); const raw = data as RawMetadata; return { content: await serialize(content), metadata: { ...raw, online: raw.online ?? false, startDate: getLocalDateFromEST( parse(raw.startDate, DATE_FORMAT, new Date()) ).toString(), // permaLink is based on the directory structure in /pages permaLink: `/events/${year}/${term}/${slug}`, year: year, term: term, slug: slug, } as Metadata, }; } export async function getEventsByTerm( year: number, term: Term ): Promise { try { return (await fs.readdir(path.join(EVENTS_PATH, year.toString(), term))) .filter((name) => name.endsWith(".md")) .map((name) => name.slice(0, -".md".length)); } catch { return []; } } export async function getUpcomingEvents(): Promise { const terms: TermYear[] = []; // Get events for the next two terms for (const termYear of getTermYear()) { if (terms.length >= 2) { break; } terms.push(termYear); } const events: Event[] = ( await Promise.all( terms.map(async ({ year, term }) => { try { const eventsInTerm = await getEventsByTerm(year, term); return await Promise.all( eventsInTerm.map((slug) => getEventBySlug(year, term, slug)) ); } catch (error) { return []; } }) ) ).flat(); return events .filter( (ev) => // use endDate if possible, else use startDate new Date(ev.metadata.endDate ?? ev.metadata.startDate).getTime() >= Date.now() ) .sort((a, b) => { return ( new Date(a.metadata.startDate).getTime() - new Date(b.metadata.startDate).getTime() ); }); } export async function getAllEvents(): Promise { const events: Event[] = []; for (const year of await getEventYears()) { for (const term of await getEventTermsByYear(year)) { for (const slug of await getEventsByTerm(year, term)) { events.push(await getEventBySlug(year, term, slug)); } } } return events; } export async function getEventsPageProps({ term, year, }: TermYear): Promise { const eventNames = await getEventsByTerm(year, term); const events: Event[] = ( await Promise.all( eventNames.map((file: string) => getEventBySlug(year, term, file)) ) ).sort( (a, b) => new Date(a.metadata.startDate).getTime() - new Date(b.metadata.startDate).getTime() ); const currentDate = Date.now(); const pastEvents = events .filter( (event) => // fallback to startDate if endDate is not set new Date(event.metadata.endDate ?? event.metadata.startDate).getTime() < currentDate ) .reverse(); const futureEvents = events.filter( // We display events that are currently going on as upcoming so they still show up homepage and other pages on the top (event) => new Date(event.metadata.endDate ?? event.metadata.startDate).getTime() >= currentDate ); const eventYears = await getEventYears(); const minYear = eventYears[0]; const pastTerms: TermYear[] = []; for (const current of getTermYear( { year, term }, { goBackwards: true, skipCurrent: true } )) { if (pastTerms.length >= 2 || current.year < minYear) { break; } if ((await getEventsByTerm(current.year, current.term)).length !== 0) { pastTerms.push(current); } } pastTerms.reverse(); const maxYear = eventYears[eventYears.length - 1]; const futureTerms: TermYear[] = []; for (const current of getTermYear( { year, term }, { goBackwards: false, skipCurrent: true } )) { if (futureTerms.length >= 2 || maxYear < current.year) { break; } if ((await getEventsByTerm(current.year, current.term)).length !== 0) { futureTerms.push(current); } } const current = getCurrentTermYear(); return { year: year, term: term, pastEvents: pastEvents, futureEvents: futureEvents, isCurrentTerm: term === current.term && year === current.year, pastTerms: pastTerms, futureTerms: futureTerms, }; }