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:
- npm run build:calendar
- name: generate-api
image: node:16
depends_on:
- install-deps
commands:
- npm run build:api
- name: build
image: node:16
depends_on:
@ -47,6 +54,7 @@ steps:
- name: export
image: node:16
depends_on:
- generate-api
- generate-calendar
- build
commands:

6
.gitignore vendored
View File

@ -28,4 +28,8 @@ yarn-error.log*
/public/events.ics
# 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;
showDescription?: boolean;
children: ReactNode;
year: string;
year: number;
term: string;
slug: string;
titleLinked: boolean;

View File

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

View File

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

View File

@ -6,28 +6,33 @@ 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";
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<string[]> {
export async function getEventYears(): Promise<number[]> {
return (await fs.readdir(EVENTS_PATH, { withFileTypes: true }))
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)
.map((dirent) => parseInt(dirent.name))
.sort();
}
export async function getEventTermsByYear(year: string): Promise<Term[]> {
export async function getEventTermsByYear(year: number): Promise<Term[]> {
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))
.map((dirent) => dirent.name as Term)
@ -55,7 +60,7 @@ interface Metadata {
location: string;
permaLink: string;
registerLink?: string;
year: string;
year: number;
term: string;
slug: string;
}
@ -66,12 +71,12 @@ export interface Event {
}
export async function getEventBySlug(
year: string,
year: number,
term: Term,
slug: string
): Promise<Event> {
const file = await fs.readFile(
path.join(EVENTS_PATH, year, term, `${slug}.md`),
path.join(EVENTS_PATH, year.toString(), term, `${slug}.md`),
"utf-8"
);
const { content, data } = matter(file);
@ -95,11 +100,11 @@ export async function getEventBySlug(
}
export async function getEventsByTerm(
year: string,
year: number,
term: Term
): Promise<string[]> {
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"))
.map((name) => name.slice(0, -".md".length));
} catch {
@ -108,22 +113,24 @@ export async function getEventsByTerm(
}
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 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(
[
{ year: currentYear.toString(), term: currentTerm },
{ year: nextYear.toString(), term: nextTerm },
].map(async ({ year, term }) => {
terms.map(async ({ year, term }) => {
try {
const eventsInTerm = await getEventsByTerm(year, TERMS[term]);
const eventsInTerm = await getEventsByTerm(year, term);
return await Promise.all(
eventsInTerm.map((slug) => getEventBySlug(year, TERMS[term], slug))
eventsInTerm.map((slug) => getEventBySlug(year, term, slug))
);
} catch (error) {
return [];
@ -162,12 +169,9 @@ export async function getAllEvents(): Promise<Event[]> {
}
export async function getEventsPageProps({
year,
term,
}: {
year: string;
term: Term;
}): Promise<Props> {
year,
}: TermYear): Promise<Props> {
const eventNames = await getEventsByTerm(year, term);
const events: Event[] = (
@ -198,40 +202,43 @@ export async function getEventsPageProps({
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);
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: { 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);
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,
@ -242,70 +249,3 @@ export async function getEventsPageProps({
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;
}
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") {
return dummyMembers;
}

View File

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

View File

@ -5,12 +5,10 @@ import matter from "gray-matter";
import { Client } from "ldapts";
import { serialize } from "next-mdx-remote/serialize";
import { getCurrentTerm } from "@/lib/events";
import { capitalize } from "@/utils";
import { capitalize, TermYear } from "@/utils";
const EXECS_PATH = path.join("content", "team", "execs");
const FILETYPE = ".md";
const { year, term } = getCurrentTerm();
const execPositions: { [position: string]: string } = {
president: "President",
@ -34,7 +32,54 @@ export interface Metadata {
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") {
return [["codey", "mascot"]];
}
@ -45,7 +90,7 @@ export async function getExecNamePosPairs() {
// position: name
const execMembers: { [position: string]: string } = {};
let formattedExec: [string, string][] = [];
let formattedExec: [person: string, position: string][] = [];
try {
await client.bind("", "");
@ -89,40 +134,6 @@ export async function getExecNamePosPairs() {
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) {
try {
await access(path.join("public", imgPath));

70
package-lock.json generated
View File

@ -45,6 +45,7 @@
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^7.0.0",
"ts-node": "^10.2.1",
"tsconfig-paths": "^4.1.0",
"typescript": "4.6.4"
},
"engines": {
@ -1075,7 +1076,7 @@
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/mdast": {
@ -2883,6 +2884,18 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz",
@ -7153,15 +7166,29 @@
}
},
"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==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz",
"integrity": "sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow==",
"dev": true,
"dependencies": {
"@types/json5": "^0.0.29",
"json5": "^1.0.1",
"json5": "^2.2.1",
"minimist": "^1.2.6",
"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": {
@ -8550,7 +8577,7 @@
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"@types/mdast": {
@ -10025,6 +10052,18 @@
"path-parse": "^1.0.7",
"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=="
},
"tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
"integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz",
"integrity": "sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow==",
"dev": true,
"requires": {
"@types/json5": "^0.0.29",
"json5": "^1.0.1",
"json5": "^2.2.1",
"minimist": "^1.2.6",
"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": {

View File

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

View File

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

View File

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

View File

@ -63,7 +63,11 @@ export const getStaticProps: GetStaticProps<Props, Params> = async (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { year, term, event } = context.params!;
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) => {
const eventsInTerm = await getEventsByTerm(year, term);
return eventsInTerm.map((event) => ({
year,
year: year.toString(),
term,
event,
}));

View File

@ -10,22 +10,22 @@ import { MiniEventCard } from "@/components/MiniEventCard";
import { Title } from "@/components/Title";
import {
Event,
getEventsPageProps,
getEventYears,
getEventTermsByYear,
getEventsPageProps,
} from "@/lib/events";
import { capitalize, Term } from "@/utils";
import { capitalize, Term, TermYear } from "@/utils";
import styles from "./index.module.css";
export interface Props {
year: string;
year: number;
term: Term;
pastEvents: Event[];
futureEvents: Event[];
isCurrentTerm: boolean;
pastTerms: { year: string; term: Term }[];
futureTerms: { year: string; term: Term }[];
pastTerms: TermYear[];
futureTerms: TermYear[];
}
export default function TermPage(props: Props) {
@ -61,7 +61,7 @@ export default function TermPage(props: Props) {
<HeaderLink
{...link}
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>
@ -129,7 +129,7 @@ export default function TermPage(props: Props) {
}
function HeaderLink(props: {
year: string;
year: number;
term: Term;
isCurrentTerm?: boolean;
}) {
@ -151,7 +151,14 @@ export const getStaticProps: GetStaticProps<Props, Params> = async (
context
) => {
// 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 () => {
@ -161,7 +168,7 @@ export const getStaticPaths: GetStaticPaths<Params> = async () => {
years.map(async (year) => {
const terms = await getEventTermsByYear(year);
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 {
props: {
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 () => {
const years = await getEventYears();
const paths = years.map((curYear) => ({
params: { year: curYear },
params: { year: curYear.toString() },
}));
return {
paths: paths,

View File

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

View File

@ -1,11 +1,12 @@
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]";
export default TermPage;
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 ical, { ICalCalendarMethod } from "ical-generator";
import { getAllEvents } from "../lib/events";
import { getAllEvents } from "@/lib/events";
export async function generateCalendar() {
const events = await getAllEvents();

View File

@ -21,3 +21,68 @@ export function getLocalDateFromEST(date: Date): Date {
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;
}