Computer Science Club of the University of Waterloo's website.
https://csclub.uwaterloo.ca
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
311 lines
7.7 KiB
311 lines
7.7 KiB
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 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,
|
|
DATE_FORMAT,
|
|
getLocalDateFromEST,
|
|
} 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;
|
|
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: string;
|
|
term: string;
|
|
slug: string;
|
|
}
|
|
|
|
export interface Event {
|
|
content: MDXRemoteSerializeResult<Record<string, unknown>>;
|
|
metadata: Metadata;
|
|
}
|
|
|
|
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,
|
|
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: 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) =>
|
|
// 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<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.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 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],
|
|
};
|
|
}
|
|
|