www-new/lib/events.ts

296 lines
7.5 KiB
TypeScript

import fs from "fs/promises";
import path from "path";
import { parse } from "date-fns";
import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
import matter from "gray-matter";
import { MDXRemoteSerializeResult } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize";
import type { Props } from "../pages/events/[year]/[term]/index";
// do not use alias "@/utils" as generate-calendar imports a function from this file and ts-node is not compatible
import { Term, TERMS, isTerm } from "../utils";
const EVENTS_PATH = path.join("content", "events");
export async function getEventYears(): Promise<string[]> {
return (await fs.readdir(EVENTS_PATH, { withFileTypes: true }))
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)
.sort();
}
export async function getEventTermsByYear(year: string): Promise<Term[]> {
return (
await fs.readdir(path.join(EVENTS_PATH, year), { 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;
date: string;
online?: boolean;
location: string;
registerLink?: string;
}
interface Metadata {
name: string;
poster?: string;
short: string;
date: string;
online: boolean;
location: string;
permaLink: string;
registerLink?: string;
}
export interface Event {
content: MDXRemoteSerializeResult<Record<string, unknown>>;
metadata: Metadata;
}
export const DATE_FORMAT = "MMMM dd yyyy HH:mm";
export async function getEventBySlug(
year: string,
term: Term,
slug: string
): Promise<Event> {
const file = await fs.readFile(
path.join(EVENTS_PATH, year, 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,
date: getLocalDateFromEST(
parse(raw.date, DATE_FORMAT, new Date())
).toString(),
// permaLink is based on the directory structure in /pages
permaLink: `/events/${year}/${term}/${slug}`,
} as Metadata,
};
}
export async function getEventsByTerm(
year: string,
term: Term
): Promise<string[]> {
try {
return (await fs.readdir(path.join(EVENTS_PATH, year, term)))
.filter((name) => name.endsWith(".md"))
.map((name) => name.slice(0, -".md".length));
} catch {
return [];
}
}
export async function getUpcomingEvents(): Promise<Event[]> {
const today = new Date();
const currentYear = today.getFullYear();
const currentTerm = Math.trunc(today.getMonth() / 4);
const nextYear = currentTerm < 2 ? currentYear : currentYear + 1;
const nextTerm = (currentTerm + 1) % 3;
const events: Event[] = (
await Promise.all(
[
{ year: currentYear.toString(), term: currentTerm },
{ year: nextYear.toString(), term: nextTerm },
].map(async ({ year, term }) => {
try {
const eventsInTerm = await getEventsByTerm(year, TERMS[term]);
return await Promise.all(
eventsInTerm.map((slug) => getEventBySlug(year, TERMS[term], slug))
);
} catch (error) {
return [];
}
})
)
).flat();
return events
.filter((ev) => new Date(ev.metadata.date).getTime() >= Date.now())
.sort((a, b) => {
return (
new Date(a.metadata.date).getTime() -
new Date(b.metadata.date).getTime()
);
});
}
export async function getAllEvents(): Promise<Event[]> {
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({
year,
term,
}: {
year: string;
term: Term;
}): Promise<Props> {
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.date).getTime() - new Date(b.metadata.date).getTime()
);
const currentDate = Date.now();
const pastEvents = events
.filter((event) => new Date(event.metadata.date).getTime() < currentDate)
.reverse();
const futureEvents = events.filter(
(event) => new Date(event.metadata.date).getTime() >= currentDate
);
const current = getCurrentTerm();
const eventYears = await getEventYears();
const minYear = eventYears[0];
const pastTerms: { year: string; term: Term }[] = [];
let curPastYear = year;
let curPastTerm = term;
while (parseInt(curPastYear) >= parseInt(minYear) && pastTerms.length < 2) {
const pastTerm = getPastTerm(curPastYear, curPastTerm);
curPastYear = pastTerm.year;
curPastTerm = pastTerm.term;
if ((await getEventsByTerm(curPastYear, curPastTerm)).length !== 0) {
pastTerms.push(pastTerm);
}
}
pastTerms.reverse();
const maxYear = eventYears[eventYears.length - 1];
const futureTerms: { year: string; term: Term }[] = [];
let curFutureYear = year;
let curFutureTerm = term;
while (
parseInt(curFutureYear) <= parseInt(maxYear) &&
futureTerms.length < 2
) {
const futureTerm = getFutureTerm(curFutureYear, curFutureTerm);
curFutureYear = futureTerm.year;
curFutureTerm = futureTerm.term;
if ((await getEventsByTerm(curFutureYear, curFutureTerm)).length !== 0) {
futureTerms.push(futureTerm);
}
}
return {
year: year,
term: term,
pastEvents: pastEvents,
futureEvents: futureEvents,
isCurrentTerm: term === current.term && year === current.year,
pastTerms: pastTerms,
futureTerms: futureTerms,
};
}
export function getCurrentTerm(): { year: string; term: Term } {
const today = new Date().toLocaleDateString("en-CA", {
timeZone: "EST",
year: "numeric",
month: "numeric",
day: "numeric",
});
const [year] = today.split("-");
let term = "";
if (`${year}-01-01` <= today) {
term = "winter";
}
if (`${year}-05-01` <= today) {
term = "spring";
}
if (`${year}-09-01` <= today) {
term = "fall";
}
if (!isTerm(term)) {
throw new Error("Error setting the current term");
}
return { year, term };
}
function getPastTerm(year: string, term: Term): { year: string; term: Term } {
const index = TERMS.indexOf(term);
if (index === -1) {
throw new Error(`[getPastTerm] Not a valid term: "${term}" "${year}"`);
}
return index === 0
? {
year: (parseInt(year) - 1).toString(),
term: TERMS[TERMS.length - 1],
}
: {
year: year,
term: TERMS[index - 1],
};
}
function getFutureTerm(year: string, term: Term): { year: string; term: Term } {
const index = TERMS.indexOf(term);
if (index === -1) {
throw new Error(`[getFutureTerm] Not a valid term: "${term}" "${year}"`);
}
return index === TERMS.length - 1
? {
year: (parseInt(year) + 1).toString(),
term: TERMS[0],
}
: {
year: year,
term: TERMS[index + 1],
};
}
// The date that's returned should be in local time
export function getLocalDateFromEST(date: Date) {
return utcToZonedTime(
// The parsed date is in EST
zonedTimeToUtc(date, "America/Toronto"),
Intl.DateTimeFormat().resolvedOptions().timeZone
);
}