Create members.json api (#489)
continuous-integration/drone/push Build is passing Details

I changed some other code along the way ... which makes this PR slightly long :') rip.

closes #472

takes a stab at #466

https://csclub.uwaterloo.ca/~a3thakra/csc/adi-member-json-api/api/members.json
Reviewed-on: #489
Reviewed-by: Amy <a258wang@csclub.uwaterloo.ca>
This commit is contained in:
Aditya Thakral 2022-08-08 04:31:15 -04:00
parent 8e0e446fd9
commit be308f6249
23 changed files with 452 additions and 430 deletions

View File

@ -37,6 +37,13 @@ steps:
commands: commands:
- npm run build:calendar - npm run build:calendar
- name: generate-api
image: node:16
depends_on:
- install-deps
commands:
- npm run build:api
- name: build - name: build
image: node:16 image: node:16
depends_on: depends_on:
@ -47,6 +54,7 @@ steps:
- name: export - name: export
image: node:16 image: node:16
depends_on: depends_on:
- generate-api
- generate-calendar - generate-calendar
- build - build
commands: commands:

6
.gitignore vendored
View File

@ -28,4 +28,8 @@ yarn-error.log*
/public/events.ics /public/events.ics
# Images should be optimized # Images should be optimized
/public/images /public/images
# APIs should be automatically generated, schema should be checked in
/public/api/*
!/public/api/schema

View File

@ -19,7 +19,7 @@ interface EventCardProps {
permaLink: string; permaLink: string;
showDescription?: boolean; showDescription?: boolean;
children: ReactNode; children: ReactNode;
year: string; year: number;
term: string; term: string;
slug: string; slug: string;
titleLinked: boolean; titleLinked: boolean;

View File

@ -14,7 +14,7 @@ interface MiniEventCardProps {
startDate: Date; startDate: Date;
endDate?: Date; endDate?: Date;
background: "dark-bg" | "normal-bg"; background: "dark-bg" | "normal-bg";
year: string; year: number;
term: string; term: string;
slug: string; slug: string;
} }

View File

@ -1,8 +1,9 @@
import { parse } from "date-fns"; import { parse } from "date-fns";
import React from "react"; import React from "react";
import { DATE_FORMAT, getLocalDateFromEST } from "@/utils";
import warnings from "../content/warnings/warnings.json"; import warnings from "../content/warnings/warnings.json";
import { DATE_FORMAT, getLocalDateFromEST } from "../utils";
import styles from "./WarningHeader.module.css"; import styles from "./WarningHeader.module.css";

View File

@ -6,28 +6,33 @@ import matter from "gray-matter";
import { MDXRemoteSerializeResult } from "next-mdx-remote"; import { MDXRemoteSerializeResult } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize"; 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 { import {
Term, Term,
TERMS, TERMS,
isTerm, isTerm,
DATE_FORMAT, DATE_FORMAT,
getLocalDateFromEST, getLocalDateFromEST,
} from "../utils"; TermYear,
getTermYear,
getCurrentTermYear,
} from "@/utils";
import type { Props } from "../pages/events/[year]/[term]";
const EVENTS_PATH = path.join("content", "events"); const EVENTS_PATH = path.join("content", "events");
export async function getEventYears(): Promise<string[]> { export async function getEventYears(): Promise<number[]> {
return (await fs.readdir(EVENTS_PATH, { withFileTypes: true })) return (await fs.readdir(EVENTS_PATH, { withFileTypes: true }))
.filter((dirent) => dirent.isDirectory()) .filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name) .map((dirent) => parseInt(dirent.name))
.sort(); .sort();
} }
export async function getEventTermsByYear(year: string): Promise<Term[]> { export async function getEventTermsByYear(year: number): Promise<Term[]> {
return ( return (
await fs.readdir(path.join(EVENTS_PATH, year), { withFileTypes: true }) await fs.readdir(path.join(EVENTS_PATH, year.toString()), {
withFileTypes: true,
})
) )
.filter((dirent) => dirent.isDirectory() && isTerm(dirent.name)) .filter((dirent) => dirent.isDirectory() && isTerm(dirent.name))
.map((dirent) => dirent.name as Term) .map((dirent) => dirent.name as Term)
@ -55,7 +60,7 @@ interface Metadata {
location: string; location: string;
permaLink: string; permaLink: string;
registerLink?: string; registerLink?: string;
year: string; year: number;
term: string; term: string;
slug: string; slug: string;
} }
@ -66,12 +71,12 @@ export interface Event {
} }
export async function getEventBySlug( export async function getEventBySlug(
year: string, year: number,
term: Term, term: Term,
slug: string slug: string
): Promise<Event> { ): Promise<Event> {
const file = await fs.readFile( const file = await fs.readFile(
path.join(EVENTS_PATH, year, term, `${slug}.md`), path.join(EVENTS_PATH, year.toString(), term, `${slug}.md`),
"utf-8" "utf-8"
); );
const { content, data } = matter(file); const { content, data } = matter(file);
@ -95,11 +100,11 @@ export async function getEventBySlug(
} }
export async function getEventsByTerm( export async function getEventsByTerm(
year: string, year: number,
term: Term term: Term
): Promise<string[]> { ): Promise<string[]> {
try { try {
return (await fs.readdir(path.join(EVENTS_PATH, year, term))) return (await fs.readdir(path.join(EVENTS_PATH, year.toString(), term)))
.filter((name) => name.endsWith(".md")) .filter((name) => name.endsWith(".md"))
.map((name) => name.slice(0, -".md".length)); .map((name) => name.slice(0, -".md".length));
} catch { } catch {
@ -108,22 +113,24 @@ export async function getEventsByTerm(
} }
export async function getUpcomingEvents(): Promise<Event[]> { export async function getUpcomingEvents(): Promise<Event[]> {
const today = new Date(); const terms: TermYear[] = [];
const currentYear = today.getFullYear();
const currentTerm = Math.trunc(today.getMonth() / 4); // Get events for the next two terms
const nextYear = currentTerm < 2 ? currentYear : currentYear + 1; for (const termYear of getTermYear()) {
const nextTerm = (currentTerm + 1) % 3; if (terms.length >= 2) {
break;
}
terms.push(termYear);
}
const events: Event[] = ( const events: Event[] = (
await Promise.all( await Promise.all(
[ terms.map(async ({ year, term }) => {
{ year: currentYear.toString(), term: currentTerm },
{ year: nextYear.toString(), term: nextTerm },
].map(async ({ year, term }) => {
try { try {
const eventsInTerm = await getEventsByTerm(year, TERMS[term]); const eventsInTerm = await getEventsByTerm(year, term);
return await Promise.all( return await Promise.all(
eventsInTerm.map((slug) => getEventBySlug(year, TERMS[term], slug)) eventsInTerm.map((slug) => getEventBySlug(year, term, slug))
); );
} catch (error) { } catch (error) {
return []; return [];
@ -162,12 +169,9 @@ export async function getAllEvents(): Promise<Event[]> {
} }
export async function getEventsPageProps({ export async function getEventsPageProps({
year,
term, term,
}: { year,
year: string; }: TermYear): Promise<Props> {
term: Term;
}): Promise<Props> {
const eventNames = await getEventsByTerm(year, term); const eventNames = await getEventsByTerm(year, term);
const events: Event[] = ( const events: Event[] = (
@ -198,40 +202,43 @@ export async function getEventsPageProps({
currentDate currentDate
); );
const current = getCurrentTerm();
const eventYears = await getEventYears(); const eventYears = await getEventYears();
const minYear = eventYears[0]; const minYear = eventYears[0];
const pastTerms: { year: string; term: Term }[] = []; const pastTerms: TermYear[] = [];
let curPastYear = year;
let curPastTerm = term; for (const current of getTermYear(
while (parseInt(curPastYear) >= parseInt(minYear) && pastTerms.length < 2) { { year, term },
const pastTerm = getPastTerm(curPastYear, curPastTerm); { goBackwards: true, skipCurrent: true }
curPastYear = pastTerm.year; )) {
curPastTerm = pastTerm.term; if (pastTerms.length >= 2 || current.year < minYear) {
if ((await getEventsByTerm(curPastYear, curPastTerm)).length !== 0) { break;
pastTerms.push(pastTerm); }
if ((await getEventsByTerm(current.year, current.term)).length !== 0) {
pastTerms.push(current);
} }
} }
pastTerms.reverse(); pastTerms.reverse();
const maxYear = eventYears[eventYears.length - 1]; const maxYear = eventYears[eventYears.length - 1];
const futureTerms: { year: string; term: Term }[] = []; const futureTerms: TermYear[] = [];
let curFutureYear = year;
let curFutureTerm = term; for (const current of getTermYear(
while ( { year, term },
parseInt(curFutureYear) <= parseInt(maxYear) && { goBackwards: false, skipCurrent: true }
futureTerms.length < 2 )) {
) { if (futureTerms.length >= 2 || maxYear < current.year) {
const futureTerm = getFutureTerm(curFutureYear, curFutureTerm); break;
curFutureYear = futureTerm.year; }
curFutureTerm = futureTerm.term;
if ((await getEventsByTerm(curFutureYear, curFutureTerm)).length !== 0) { if ((await getEventsByTerm(current.year, current.term)).length !== 0) {
futureTerms.push(futureTerm); futureTerms.push(current);
} }
} }
const current = getCurrentTermYear();
return { return {
year: year, year: year,
term: term, term: term,
@ -242,70 +249,3 @@ export async function getEventsPageProps({
futureTerms: futureTerms, 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],
};
}

View File

@ -8,7 +8,7 @@ export interface Member {
program: string; program: string;
} }
export async function getMembers(year: string, term: Term): Promise<Member[]> { export async function getMembers(year: number, term: Term): Promise<Member[]> {
if (process.env.USE_LDAP?.toLowerCase() !== "true") { if (process.env.USE_LDAP?.toLowerCase() !== "true") {
return dummyMembers; return dummyMembers;
} }

View File

@ -7,9 +7,16 @@ import truncateMarkdown from "markdown-truncate";
import { MDXRemoteSerializeResult } from "next-mdx-remote"; import { MDXRemoteSerializeResult } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize"; import { serialize } from "next-mdx-remote/serialize";
import { isTerm, Term, TERMS } from "@/utils"; import {
isTerm,
import { DATE_FORMAT, getLocalDateFromEST } from "../utils"; Term,
TERMS,
DATE_FORMAT,
getLocalDateFromEST,
TermYear,
getTermYear,
getCurrentTermYear,
} from "@/utils";
export const NEWS_PATH = path.join("content", "news"); export const NEWS_PATH = path.join("content", "news");
@ -96,23 +103,27 @@ export async function getNewsBySlug(
} }
export async function getRecentNews(): Promise<News[]> { export async function getRecentNews(): Promise<News[]> {
const today = new Date(); const terms: TermYear[] = [];
const currentYear = today.getFullYear();
const currentTerm = Math.trunc(today.getMonth() / 4); // Get news for the last two terms
const prevYear = currentTerm > 0 ? currentYear : currentYear - 1; for (const termYear of getTermYear(getCurrentTermYear(), {
const prevTerm = (currentTerm - 1 + 3) % 3; goBackwards: true,
})) {
if (terms.length >= 2) {
break;
}
terms.push(termYear);
}
const news: News[] = ( const news: News[] = (
await Promise.all( await Promise.all(
[ terms.map(async ({ year, term }) => {
{ year: currentYear.toString(), term: currentTerm },
{ year: prevYear.toString(), term: prevTerm },
].map(async ({ year, term }) => {
try { try {
const newsInTerm = await getNewsByTerm(year, TERMS[term]); const newsInTerm = await getNewsByTerm(year.toString(), term);
return await Promise.all( return await Promise.all(
newsInTerm.map((slug) => { newsInTerm.map((slug) => {
return getNewsBySlug(year, TERMS[term], slug, true); return getNewsBySlug(year.toString(), term, slug, true);
}) })
); );
} catch (error) { } catch (error) {

View File

@ -5,12 +5,10 @@ import matter from "gray-matter";
import { Client } from "ldapts"; import { Client } from "ldapts";
import { serialize } from "next-mdx-remote/serialize"; import { serialize } from "next-mdx-remote/serialize";
import { getCurrentTerm } from "@/lib/events"; import { capitalize, TermYear } from "@/utils";
import { capitalize } from "@/utils";
const EXECS_PATH = path.join("content", "team", "execs"); const EXECS_PATH = path.join("content", "team", "execs");
const FILETYPE = ".md"; const FILETYPE = ".md";
const { year, term } = getCurrentTerm();
const execPositions: { [position: string]: string } = { const execPositions: { [position: string]: string } = {
president: "President", president: "President",
@ -34,7 +32,54 @@ export interface Metadata {
image: string; image: string;
} }
export async function getExecNamePosPairs() { export async function getExecs(termYear: TermYear) {
const execNamePosPairs = await getExecNamePosPairs(termYear);
return await Promise.all(
execNamePosPairs.map((namePosPair) =>
getExec(namePosPair[0], namePosPair[1])
)
);
}
async function getExec(name: string, pos: string) {
let content, metadata;
try {
const raw = await readFile(path.join(EXECS_PATH, `${name}${FILETYPE}`));
({ content, data: metadata } = matter(raw));
const image = await getMemberImagePath(metadata.name as string);
return {
content: await serialize(content),
metadata: { ...metadata, image } as Metadata,
};
} catch (err) {
// Capitalize the first letter of the first name and last name
const firstName = capitalize(name.split("-")[0]);
const lastName = capitalize(name.split("-")[1]);
const posName = execPositions[pos];
content = "Coming Soon!";
metadata = {
name: `${firstName} ${lastName}`,
role: `${posName}`,
};
const image = await getMemberImagePath(metadata.name);
return {
content: await serialize(content),
metadata: { ...metadata, image } as Metadata,
};
}
}
async function getExecNamePosPairs({
term,
year,
}: TermYear): Promise<[person: string, position: string][]> {
if (process.env.USE_LDAP?.toLowerCase() !== "true") { if (process.env.USE_LDAP?.toLowerCase() !== "true") {
return [["codey", "mascot"]]; return [["codey", "mascot"]];
} }
@ -45,7 +90,7 @@ export async function getExecNamePosPairs() {
// position: name // position: name
const execMembers: { [position: string]: string } = {}; const execMembers: { [position: string]: string } = {};
let formattedExec: [string, string][] = []; let formattedExec: [person: string, position: string][] = [];
try { try {
await client.bind("", ""); await client.bind("", "");
@ -89,40 +134,6 @@ export async function getExecNamePosPairs() {
return formattedExec; return formattedExec;
} }
export async function getExec(name: string, pos: string, convert = true) {
let content, metadata;
try {
const raw = await readFile(path.join(EXECS_PATH, `${name}${FILETYPE}`));
({ content, data: metadata } = matter(raw));
const image = await getMemberImagePath(metadata.name as string);
return {
content: convert ? await serialize(content) : content,
metadata: { ...metadata, image } as Metadata,
};
} catch (err) {
// Capitalize the first letter of the first name and last name
const firstName = capitalize(name.split("-")[0]);
const lastName = capitalize(name.split("-")[1]);
const posName = execPositions[pos];
content = "Coming Soon!";
metadata = {
name: `${firstName} ${lastName}`,
role: `${posName}`,
};
const image = await getMemberImagePath(metadata.name);
return {
content: convert ? await serialize(content) : content,
metadata: { ...metadata, image } as Metadata,
};
}
}
async function getImage(imgPath: string) { async function getImage(imgPath: string) {
try { try {
await access(path.join("public", imgPath)); await access(path.join("public", imgPath));

70
package-lock.json generated
View File

@ -45,6 +45,7 @@
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^7.0.0", "postcss-preset-env": "^7.0.0",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"tsconfig-paths": "^4.1.0",
"typescript": "4.6.4" "typescript": "4.6.4"
}, },
"engines": { "engines": {
@ -1075,7 +1076,7 @@
"node_modules/@types/json5": { "node_modules/@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"node_modules/@types/mdast": { "node_modules/@types/mdast": {
@ -2883,6 +2884,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/eslint-plugin-import/node_modules/tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
"integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
"dev": true,
"dependencies": {
"@types/json5": "^0.0.29",
"json5": "^1.0.1",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
}
},
"node_modules/eslint-plugin-prettier": { "node_modules/eslint-plugin-prettier": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz",
@ -7153,15 +7166,29 @@
} }
}, },
"node_modules/tsconfig-paths": { "node_modules/tsconfig-paths": {
"version": "3.14.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz",
"integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", "integrity": "sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/json5": "^0.0.29", "json5": "^2.2.1",
"json5": "^1.0.1",
"minimist": "^1.2.6", "minimist": "^1.2.6",
"strip-bom": "^3.0.0" "strip-bom": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
@ -8550,7 +8577,7 @@
"@types/json5": { "@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"@types/mdast": { "@types/mdast": {
@ -10025,6 +10052,18 @@
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
} }
},
"tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
"integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
"dev": true,
"requires": {
"@types/json5": "^0.0.29",
"json5": "^1.0.1",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
}
} }
} }
}, },
@ -12975,15 +13014,22 @@
"integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==" "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw=="
}, },
"tsconfig-paths": { "tsconfig-paths": {
"version": "3.14.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz",
"integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", "integrity": "sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/json5": "^0.0.29", "json5": "^2.2.1",
"json5": "^1.0.1",
"minimist": "^1.2.6", "minimist": "^1.2.6",
"strip-bom": "^3.0.0" "strip-bom": "^3.0.0"
},
"dependencies": {
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true
}
} }
}, },
"tslib": { "tslib": {

View File

@ -7,10 +7,11 @@
}, },
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "npm run build:images && npm run build:web && npm run build:calendar", "build": "npm run build:images && npm run build:web && npm run build:calendar && npm run build:api",
"build:images": "ts-node ./scripts/optimize-images",
"build:web": "next build", "build:web": "next build",
"build:calendar": "ts-node ./scripts/generate-calendar", "build:images": "ts-node -r tsconfig-paths/register ./scripts/optimize-images",
"build:calendar": "ts-node -r tsconfig-paths/register ./scripts/generate-calendar",
"build:api": "ts-node -r tsconfig-paths/register ./scripts/api/members",
"start": "next start", "start": "next start",
"export": "next export", "export": "next export",
"lint": "eslint \"{pages,components,lib,hooks,scripts}/**/*.{js,ts,tsx,jsx}\" --quiet", "lint": "eslint \"{pages,components,lib,hooks,scripts}/**/*.{js,ts,tsx,jsx}\" --quiet",
@ -56,6 +57,7 @@
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^7.0.0", "postcss-preset-env": "^7.0.0",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"tsconfig-paths": "^4.1.0",
"typescript": "4.6.4" "typescript": "4.6.4"
} }
} }

View File

@ -4,15 +4,14 @@ import React from "react";
import { Link } from "@/components/Link"; import { Link } from "@/components/Link";
import { Table } from "@/components/Table"; import { Table } from "@/components/Table";
import { Title } from "@/components/Title"; import { Title } from "@/components/Title";
import { getCurrentTerm } from "@/lib/events";
import { getMembers, Member } from "@/lib/members"; import { getMembers, Member } from "@/lib/members";
import { Term, capitalize } from "@/utils"; import { Term, capitalize, getCurrentTermYear } from "@/utils";
import styles from "./members.module.css"; import styles from "./members.module.css";
interface Props { interface Props {
members: Member[]; members: Member[];
year: string; year: number;
term: Term; term: Term;
} }
@ -56,7 +55,7 @@ export default function Members(props: Props) {
} }
export const getStaticProps: GetStaticProps<Props> = async () => { export const getStaticProps: GetStaticProps<Props> = async () => {
const curTerm = getCurrentTerm(); const curTerm = getCurrentTermYear();
return { return {
props: { props: {
year: curTerm.year, year: curTerm.year,

View File

@ -10,11 +10,11 @@ import { TeamMember } from "@/components/TeamMember";
import { TeamMemberCard } from "@/components/TeamMemberCard"; import { TeamMemberCard } from "@/components/TeamMemberCard";
import { Title } from "@/components/Title"; import { Title } from "@/components/Title";
import { import {
getExec, getExecs,
getExecNamePosPairs, Metadata as TeamMemberData,
Metadata,
getMemberImagePath, getMemberImagePath,
} from "@/lib/team"; } from "@/lib/team";
import { getCurrentTermYear } from "@/utils";
import designData from "../../content/team/design-team.json"; import designData from "../../content/team/design-team.json";
import discordData from "../../content/team/discord-team.json"; import discordData from "../../content/team/discord-team.json";
@ -31,88 +31,21 @@ import styles from "./team.module.css";
interface SerializedExec { interface SerializedExec {
content: MDXRemoteSerializeResult; content: MDXRemoteSerializeResult;
metadata: Metadata; metadata: TeamMemberData;
}
interface Team {
id: string;
name: string;
members: TeamMemberData[];
} }
interface Props { interface Props {
execs: SerializedExec[]; execs: SerializedExec[];
design: Metadata[]; teams: Team[];
discord: Metadata[];
events: Metadata[];
external: Metadata[];
marketing: Metadata[];
photography: Metadata[];
representatives: Metadata[];
website: Metadata[];
systems: Metadata[];
terminal: Metadata[];
} }
export default function Team({ export default function Team({ execs, teams }: Props) {
execs,
design,
discord,
events,
external,
marketing,
photography,
representatives,
website,
systems,
terminal,
}: Props) {
const teams = [
{
id: "reps",
name: "Community Representatives",
members: representatives,
},
{
id: "design",
name: "Design Team",
members: design,
},
{
id: "discord",
name: "Discord Team",
members: discord,
},
{
id: "events",
name: "Events Team",
members: events,
},
{
id: "external",
name: "External Affairs Team",
members: external,
},
{
id: "marketing",
name: "Marketing Team",
members: marketing,
},
{
id: "photography",
name: "Photography Team",
members: photography,
},
{
id: "website",
name: "Web Committee",
members: website,
},
{
id: "system",
name: "Systems Committee",
members: systems,
},
{
id: "terminal",
name: "Terminal Committee",
members: terminal,
},
];
return ( return (
<> <>
<Title>Team</Title> <Title>Team</Title>
@ -122,16 +55,11 @@ export default function Team({
<h1 className={styles.header}>Meet the Team!</h1> <h1 className={styles.header}>Meet the Team!</h1>
<div className={styles.nav}> <div className={styles.nav}>
<Link href="#execs">The Executives</Link> <Link href="#execs">The Executives</Link>
<Link href="#reps">Community Representatives</Link> {teams.map((team) => (
<Link href="#design">Design</Link> <Link href={`#${team.id}`} key={team.id}>
<Link href="#discord">Discord</Link> {team.name}
<Link href="#events">Events</Link> </Link>
<Link href="#external">External Affairs</Link> ))}
<Link href="#marketing">Marketing</Link>
<Link href="#photography">Photography</Link>
<Link href="#website">Web Committee</Link>
<Link href="#system">Systems Committee</Link>
<Link href="#terminal">Terminal Committee</Link>
</div> </div>
<h2 <h2
className={styles.subheading} className={styles.subheading}
@ -180,7 +108,7 @@ Team.Layout = function TeamLayout(props: { children: React.ReactNode }) {
}; };
interface MembersProps { interface MembersProps {
team: Metadata[]; team: TeamMemberData[];
} }
function MembersList(props: MembersProps) { function MembersList(props: MembersProps) {
@ -193,93 +121,104 @@ function MembersList(props: MembersProps) {
); );
} }
type TeamMember = Omit<Metadata, "image"> & { image?: string }; async function getTeamWithImages(team: Team): Promise<Team> {
return {
async function getTeamWithImages(team: TeamMember[]) { ...team,
return await Promise.all( members: await Promise.all(
team.map(async (member) => { team.members.map(async (member) => {
const image = member.image ?? (await getMemberImagePath(member.name)); const image = member.image ?? (await getMemberImagePath(member.name));
return { return {
...member, ...member,
image, image,
}; };
}) })
); ),
};
} }
function memberComparer(a: Metadata, b: Metadata) { function memberComparer(a: TeamMemberData, b: TeamMemberData) {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
} }
function sortTeam(team: Metadata[]): Metadata[] { function sortTeamMembers(team: Team): Team {
const leads = team const leads = team.members
.filter(({ role }) => role === "Team Lead") .filter(({ role }) => role === "Team Lead")
.sort(memberComparer); .sort(memberComparer);
const general = team.filter(({ role }) => !role).sort(memberComparer);
const others = team const general = team.members.filter(({ role }) => !role).sort(memberComparer);
const others = team.members
.filter(({ role }) => !!role && role !== "Team Lead") .filter(({ role }) => !!role && role !== "Team Lead")
.sort(memberComparer); .sort(memberComparer);
return [...leads, ...general, ...others]; return { ...team, members: [...leads, ...general, ...others] };
} }
export const getStaticProps: GetStaticProps<Props> = async () => { export const getStaticProps: GetStaticProps<Props> = async () => {
const execNamePosPairs = await getExecNamePosPairs(); const execs = await getExecs(getCurrentTermYear());
const execs = (await Promise.all( // Note that rawTeams do not contain image paths of members, even though
execNamePosPairs.map((namePosPair) => // TypeScript thinks that it does. It's just to simplify some code.
getExec(namePosPair[0], namePosPair[1]) const rawTeams = [
) {
)) as SerializedExec[]; id: "reps",
name: "Community Representatives",
members: repsData,
},
{
id: "design",
name: "Design",
members: designData,
},
{
id: "discord",
name: "Discord",
members: discordData,
},
{
id: "events",
name: "Events",
members: eventsData,
},
{
id: "external",
name: "External Affairs",
members: externalData,
},
{
id: "marketing",
name: "Marketing",
members: marketingData,
},
{
id: "photography",
name: "Photography",
members: photographyData,
},
{
id: "website",
name: "Web Committee",
members: webData,
},
{
id: "system",
name: "Systems Committee",
members: systemsData,
},
{
id: "terminal",
name: "Terminal Committee",
members: terminalData,
},
] as Team[];
let [ const teamsWithImages = await Promise.all(rawTeams.map(getTeamWithImages));
design, const teamsAfterSorting = teamsWithImages.map(sortTeamMembers);
discord,
events,
external,
marketing,
photography,
representatives,
website,
systems,
terminal,
] = await Promise.all([
getTeamWithImages(designData),
getTeamWithImages(discordData),
getTeamWithImages(eventsData),
getTeamWithImages(externalData),
getTeamWithImages(marketingData),
getTeamWithImages(photographyData),
getTeamWithImages(repsData),
getTeamWithImages(webData),
getTeamWithImages(systemsData),
getTeamWithImages(terminalData),
]);
design = sortTeam(design);
discord = sortTeam(discord);
events = sortTeam(events);
external = sortTeam(external);
marketing = sortTeam(marketing);
representatives = sortTeam(representatives);
photography = sortTeam(photography);
website = sortTeam(website);
systems = sortTeam(systems);
terminal = sortTeam(terminal);
return { return {
props: { props: {
execs, execs,
design, teams: teamsAfterSorting,
discord,
events,
external,
marketing,
photography,
representatives,
website,
systems,
terminal,
}, },
}; };
}; };

View File

@ -63,7 +63,11 @@ export const getStaticProps: GetStaticProps<Props, Params> = async (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { year, term, event } = context.params!; const { year, term, event } = context.params!;
return { return {
props: { year, term, event: await getEventBySlug(year, term, event) }, props: {
year,
term,
event: await getEventBySlug(parseInt(year), term, event),
},
}; };
}; };
@ -77,7 +81,7 @@ export const getStaticPaths: GetStaticPaths<Params> = async () => {
termsInYear.map(async (term) => { termsInYear.map(async (term) => {
const eventsInTerm = await getEventsByTerm(year, term); const eventsInTerm = await getEventsByTerm(year, term);
return eventsInTerm.map((event) => ({ return eventsInTerm.map((event) => ({
year, year: year.toString(),
term, term,
event, event,
})); }));

View File

@ -10,22 +10,22 @@ import { MiniEventCard } from "@/components/MiniEventCard";
import { Title } from "@/components/Title"; import { Title } from "@/components/Title";
import { import {
Event, Event,
getEventsPageProps,
getEventYears, getEventYears,
getEventTermsByYear, getEventTermsByYear,
getEventsPageProps,
} from "@/lib/events"; } from "@/lib/events";
import { capitalize, Term } from "@/utils"; import { capitalize, Term, TermYear } from "@/utils";
import styles from "./index.module.css"; import styles from "./index.module.css";
export interface Props { export interface Props {
year: string; year: number;
term: Term; term: Term;
pastEvents: Event[]; pastEvents: Event[];
futureEvents: Event[]; futureEvents: Event[];
isCurrentTerm: boolean; isCurrentTerm: boolean;
pastTerms: { year: string; term: Term }[]; pastTerms: TermYear[];
futureTerms: { year: string; term: Term }[]; futureTerms: TermYear[];
} }
export default function TermPage(props: Props) { export default function TermPage(props: Props) {
@ -61,7 +61,7 @@ export default function TermPage(props: Props) {
<HeaderLink <HeaderLink
{...link} {...link}
isCurrentTerm={link.year === props.year && link.term === props.term} isCurrentTerm={link.year === props.year && link.term === props.term}
key={link.year + link.term} key={`${link.year}${link.term}`}
/> />
))} ))}
<Link href="/events/archive">Archive</Link> <Link href="/events/archive">Archive</Link>
@ -129,7 +129,7 @@ export default function TermPage(props: Props) {
} }
function HeaderLink(props: { function HeaderLink(props: {
year: string; year: number;
term: Term; term: Term;
isCurrentTerm?: boolean; isCurrentTerm?: boolean;
}) { }) {
@ -151,7 +151,14 @@ export const getStaticProps: GetStaticProps<Props, Params> = async (
context context
) => { ) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return { props: await getEventsPageProps(context.params!) }; const params = context.params!;
return {
props: await getEventsPageProps({
year: parseInt(params.year),
term: params.term,
}),
};
}; };
export const getStaticPaths: GetStaticPaths<Params> = async () => { export const getStaticPaths: GetStaticPaths<Params> = async () => {
@ -161,7 +168,7 @@ export const getStaticPaths: GetStaticPaths<Params> = async () => {
years.map(async (year) => { years.map(async (year) => {
const terms = await getEventTermsByYear(year); const terms = await getEventTermsByYear(year);
return terms.map((curTerm) => ({ return terms.map((curTerm) => ({
params: { year: year, term: curTerm }, params: { year: year.toString(), term: curTerm },
})); }));
}) })
) )

View File

@ -47,7 +47,7 @@ export const getStaticProps: GetStaticProps<Props, Params> = async (
return { return {
props: { props: {
year: year, year: year,
terms: await getEventTermsByYear(year), terms: await getEventTermsByYear(parseInt(year)),
}, },
}; };
}; };
@ -55,7 +55,7 @@ export const getStaticProps: GetStaticProps<Props, Params> = async (
export const getStaticPaths: GetStaticPaths<Params> = async () => { export const getStaticPaths: GetStaticPaths<Params> = async () => {
const years = await getEventYears(); const years = await getEventYears();
const paths = years.map((curYear) => ({ const paths = years.map((curYear) => ({
params: { year: curYear }, params: { year: curYear.toString() },
})); }));
return { return {
paths: paths, paths: paths,

View File

@ -9,7 +9,7 @@ export const getStaticProps: GetStaticProps<Props> = async () => {
const years = (await getEventYears()).reverse(); const years = (await getEventYears()).reverse();
const yearsWithTerms = await Promise.all( const yearsWithTerms = await Promise.all(
years.map(async (year) => ({ years.map(async (year) => ({
year, year: year.toString(),
terms: (await getEventTermsByYear(year)).reverse(), terms: (await getEventTermsByYear(year)).reverse(),
})) }))
); );

View File

@ -1,11 +1,12 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { getCurrentTerm, getEventsPageProps } from "@/lib/events"; import { getEventsPageProps } from "@/lib/events";
import { getCurrentTermYear } from "@/utils";
import TermPage, { Props } from "./[year]/[term]"; import TermPage, { Props } from "./[year]/[term]";
export default TermPage; export default TermPage;
export const getStaticProps: GetStaticProps<Props> = async () => { export const getStaticProps: GetStaticProps<Props> = async () => {
return { props: await getEventsPageProps(getCurrentTerm()) }; return { props: await getEventsPageProps(getCurrentTermYear()) };
}; };

View File

@ -0,0 +1,25 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Member list",
"description": "List of all current members of the Computer Science Club of the University of Waterloo",
"type": "object",
"properties": {
"members": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string"
},
"program": {
"type": "string"
}
}
}
}
}
}

19
scripts/api/members.ts Normal file
View File

@ -0,0 +1,19 @@
import { writeFile } from "fs/promises";
import { getMembers } from "@/lib/members";
import { getCurrentTermYear } from "@/utils";
async function createMembersApi() {
const { term, year } = getCurrentTermYear();
const members = await getMembers(year, term);
const result = {
$schema: "https://json-schema.org/draft/2020-12/schema",
$id: "schema/members.json",
members,
};
await writeFile("public/api/members.json", JSON.stringify(result));
}
void createMembersApi();

View File

@ -1,60 +0,0 @@
import fs from "fs/promises";
import path from "path";
import { format } from "date-fns";
import { DATE_FORMAT } from "@/utils";
/*
Note:
This script will not work for events by default anymore, since events now have startDate instead of endDate
*/
import {
getEventsByTerm,
getEventTermsByYear,
getEventYears,
} from "../lib/events";
import {
getNewsByTerm,
getNewsTermsByYear,
getNewsYears,
NEWS_PATH,
} from "../lib/news";
const EVENTS_PATH = path.join("content", "events");
export async function main() {
for (const year of await getEventYears()) {
for (const term of await getEventTermsByYear(year)) {
for (const slug of await getEventsByTerm(year, term)) {
const filePath = path.join(EVENTS_PATH, year, term, `${slug}.md`);
const file = await fs.readFile(filePath, "utf-8");
await fs.writeFile(filePath, replaceDate(file));
}
}
}
for (const year of await getNewsYears()) {
for (const term of await getNewsTermsByYear(year)) {
for (const slug of await getNewsByTerm(year, term)) {
const filePath = path.join(NEWS_PATH, year, term, `${slug}.md`);
const file = await fs.readFile(filePath, "utf-8");
await fs.writeFile(filePath, replaceDate(file));
}
}
}
}
function replaceDate(file: string) {
const lines = file.split("\n");
const dateLineIdx = lines.findIndex((line) => line.startsWith("date: "));
const dateLine = lines[dateLineIdx];
const date = new Date(dateLine.slice("date: ".length + 1, -1));
lines[dateLineIdx] = `date: '${format(date, DATE_FORMAT)}'`;
return lines.join("\n");
}
void main();

View File

@ -4,7 +4,7 @@ import path from "path";
import { addHours } from "date-fns"; import { addHours } from "date-fns";
import ical, { ICalCalendarMethod } from "ical-generator"; import ical, { ICalCalendarMethod } from "ical-generator";
import { getAllEvents } from "../lib/events"; import { getAllEvents } from "@/lib/events";
export async function generateCalendar() { export async function generateCalendar() {
const events = await getAllEvents(); const events = await getAllEvents();

View File

@ -21,3 +21,68 @@ export function getLocalDateFromEST(date: Date): Date {
Intl.DateTimeFormat().resolvedOptions().timeZone Intl.DateTimeFormat().resolvedOptions().timeZone
); );
} }
export interface TermYear {
term: Term;
year: number;
}
export interface GetTermYearOptions {
goBackwards?: boolean;
skipCurrent?: boolean;
}
export function* getTermYear(
start?: number | TermYear,
{ goBackwards = false, skipCurrent = false }: GetTermYearOptions = {}
) {
const allTerms = [...TERMS];
if (goBackwards) {
allTerms.reverse();
}
const today = new Date();
const todayYear = today.getFullYear();
const todayTerm = TERMS[Math.trunc(today.getMonth() / 4)];
start ??= { term: todayTerm, year: todayYear };
if (typeof start === "number") {
start = { term: allTerms[0], year: start };
}
let currentYear = start.year;
while (0 <= currentYear && currentYear <= Number.MAX_SAFE_INTEGER) {
for (const currentTerm of allTerms) {
if (
currentYear === start.year &&
allTerms.indexOf(currentTerm) < allTerms.indexOf(start.term)
) {
continue;
}
if (
skipCurrent &&
currentYear === start.year &&
currentTerm === start.term
) {
continue;
}
yield { term: currentTerm, year: currentYear };
}
currentYear = currentYear + (goBackwards ? -1 : 1);
}
}
export function getCurrentTermYear() {
const result = getTermYear().next();
if (result.done === true) {
throw new Error("Cannot get current term. Iterator is done.");
}
return result.value;
}