diff --git a/.gitignore b/.gitignore index 238e770b..922d92a5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ # debug npm-debug.log* yarn-debug.log* -yarn-error.log* \ No newline at end of file +yarn-error.log* diff --git a/lib/events.ts b/lib/events.ts index 0a0e9aab..a909434d 100644 --- a/lib/events.ts +++ b/lib/events.ts @@ -5,8 +5,10 @@ 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"; + const EVENTS_PATH = path.join("content", "events"); -const TERMS = ["winter", "spring", "fall"]; +export const TERMS = ["winter", "spring", "fall"]; export async function getEventYears(): Promise { return (await fs.readdir(EVENTS_PATH, { withFileTypes: true })) @@ -58,9 +60,13 @@ export async function getEventsByTerm( year: string, term: string ): Promise { - return (await fs.readdir(path.join(EVENTS_PATH, year, term))) - .filter((name) => name.endsWith(".md")) - .map((name) => name.slice(0, -".md".length)); + 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 { @@ -97,3 +103,135 @@ export async function getUpcomingEvents(): Promise { ); }); } + +export async function getProps(year: string, term: string): 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.date).getTime() - new Date(b.metadata.date).getTime() + ); + + const pastEvents = events + .filter((event) => new Date(event.metadata.date).getTime() < Date.now()) + .reverse(); + + const futureEvents = events.filter( + (event) => new Date(event.metadata.date).getTime() >= Date.now() + ); + + const current = getCurrentTerm(); + + const eventYears = await getEventYears(); + + const minYear = eventYears[0]; + const pastTerms: { year: string; term: string }[] = []; + 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: string }[] = []; + 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: string } { + const date = new Date(); + let term = ""; + const year = date.getUTCFullYear().toString(); + + if ( + new Date(`${year}-01-01 EST`).getTime() <= date.getTime() && + date.getTime() <= new Date(`${year}-04-30 EST`).getTime() + ) { + term = "winter"; + } else if ( + new Date(`${year}-05-01 EST`).getTime() <= date.getTime() && + date.getTime() <= new Date(`${year}-08-31 EST`).getTime() + ) { + term = "spring"; + } else if ( + new Date(`${year}-09-01 EST`).getTime() <= date.getTime() && + date.getTime() <= new Date(`${year}-12-31 EST`).getTime() + ) { + term = "fall"; + } + + return { year, term }; +} + +function getPastTerm( + year: string, + term: string +): { year: string; term: string } { + const index = TERMS.indexOf(term); + + if (index === -1) { + throw new Error("Not a valid term"); + } + + return index === 0 + ? { + year: (parseInt(year) - 1).toString(), + term: TERMS[TERMS.length - 1], + } + : { + year: year, + term: TERMS[index - 1], + }; +} + +function getFutureTerm( + year: string, + term: string +): { year: string; term: string } { + const index = TERMS.indexOf(term); + + if (index === -1) { + throw new Error("Not a valid term"); + } + + return index === TERMS.length - 1 + ? { + year: (parseInt(year) + 1).toString(), + term: TERMS[0], + } + : { + year: year, + term: TERMS[index + 1], + }; +} diff --git a/pages/events.mdx b/pages/events.mdx deleted file mode 100644 index 0d6b3934..00000000 --- a/pages/events.mdx +++ /dev/null @@ -1 +0,0 @@ -# Events page diff --git a/pages/events/[year]/[term]/index.module.css b/pages/events/[year]/[term]/index.module.css new file mode 100644 index 00000000..ab651690 --- /dev/null +++ b/pages/events/[year]/[term]/index.module.css @@ -0,0 +1,33 @@ +.main { + margin-top: calc(60rem / 16); + margin-bottom: calc(60rem / 16); +} + +.main > h2 { + padding-bottom: 1rem; + border-bottom: 1px solid var(--primary-heading); +} + +@media only screen and (max-width: calc(768rem / 16)) { + .main { + margin-top: calc(60rem / 16); + } +} + +.header a { + color: var(--text); + font-size: calc(18rem / 16); + margin-right: calc(30rem / 16); +} + +.header a .curTerm { + color: var(--primary-accent); +} + +.blue { + color: var(--primary-accent); +} + +.miniEventCards { + margin-top: calc(30rem / 16); +} diff --git a/pages/events/[year]/[term]/index.tsx b/pages/events/[year]/[term]/index.tsx new file mode 100644 index 00000000..34a8c8e4 --- /dev/null +++ b/pages/events/[year]/[term]/index.tsx @@ -0,0 +1,154 @@ +import { ParsedUrlQuery } from "querystring"; + +import { GetStaticPaths, GetStaticProps } from "next"; +import { MDXRemote } from "next-mdx-remote"; +import React from "react"; + +import { EventCard } from "@/components/EventCard"; +import { Link } from "@/components/Link"; +import { MiniEventCard } from "@/components/MiniEventCard"; +import { + Event, + getProps, + getEventYears, + getEventTermsByYear, +} from "@/lib/events"; + +import styles from "./index.module.css"; + +export interface Props { + year: string; + term: string; + pastEvents: Event[]; + futureEvents: Event[]; + isCurrentTerm: boolean; + pastTerms: { year: string; term: string }[]; + futureTerms: { year: string; term: string }[]; +} + +export default function Term(props: Props) { + let headerTerms = [{ year: props.year, term: props.term }]; + + // p, Current, f + if (props.pastTerms.length > 0 && props.futureTerms.length > 0) { + headerTerms = [ + ...props.pastTerms.slice(-1), + ...headerTerms, + ...props.futureTerms.slice(0, 1), + ]; + } + // p, p, Current + else if (props.pastTerms.length > 0) { + headerTerms = [...props.pastTerms.slice(-2), ...headerTerms]; + } + // Current, f, f + else { + headerTerms = [...headerTerms, ...props.futureTerms.slice(0, 2)]; + } + + headerTerms.reverse(); + + const hasPastEvents = props.pastEvents.length !== 0; + const hasFutureEvents = props.futureEvents.length !== 0; + + return ( +
+
+ {headerTerms.map((link) => ( + + ))} + Archives +
+ {hasFutureEvents && ( + <> +

Upcoming Events

+
+ {props.futureEvents.map(({ content, metadata }) => ( + + + + ))} +
+ + )} + {hasPastEvents && props.isCurrentTerm &&

Past Events

} + {hasPastEvents && !props.isCurrentTerm && ( +

+ Events Archive: + + {` ${capitalize(props.term)} ${props.year}`} + +

+ )} +
+ {props.pastEvents.map(({ content, metadata }) => ( + } + key={metadata.name + metadata.date.toString()} + /> + ))} +
+
+ ); +} + +function HeaderLink(props: { + year: string; + term: string; + isCurrentTerm?: boolean; +}) { + return ( + + + {`${capitalize(props.term)} ${props.year}`} + + + ); +} + +interface Params extends ParsedUrlQuery { + year: string; + term: string; +} + +export const getStaticProps: GetStaticProps = async ( + context +) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { year, term } = context.params!; + + return { props: await getProps(year, term) }; +}; + +export const getStaticPaths: GetStaticPaths = async () => { + const years = await getEventYears(); + const paths = ( + await Promise.all( + years.map(async (year) => { + const terms = await getEventTermsByYear(year); + return terms.map((curTerm) => ({ + params: { year: year, term: curTerm }, + })); + }) + ) + ).flat(); + + return { + paths: paths, + fallback: false, + }; +}; + +function capitalize(str: string) { + return str.slice(0, 1).toUpperCase() + str.slice(1); +}