diff --git a/components/NewsCard.module.css b/components/NewsCard.module.css index fd83dd67..735ab630 100644 --- a/components/NewsCard.module.css +++ b/components/NewsCard.module.css @@ -3,6 +3,7 @@ max-width: calc(524rem / 16); background-color: var(--primary-background); border-radius: calc(20rem / 16); + margin-bottom: 1rem; } .fit.card { @@ -30,6 +31,7 @@ padding: 0; max-width: unset; background-color: transparent; + border-radius: 0; } .date { diff --git a/components/NewsCard.tsx b/components/NewsCard.tsx index 16a31be3..5aac03e8 100644 --- a/components/NewsCard.tsx +++ b/components/NewsCard.tsx @@ -1,11 +1,14 @@ import React, { ReactNode } from "react"; +import { Link } from "./Link"; + import styles from "./NewsCard.module.css"; interface NewsCardProps { date: Date; author: string; children: ReactNode; + permalink: string; fit?: boolean; } @@ -13,7 +16,8 @@ export const NewsCard: React.FC = ({ date, author, children, - fit = false, + permalink, + fit = false, // resizes the article to fit the parent container if it's not a mini card }) => { const classes = fit ? [styles.card, styles.fit] : [styles.card]; @@ -30,6 +34,11 @@ export const NewsCard: React.FC = ({
{author}
{children}
+ {!fit && ( + + Learn more + + )} ); }; diff --git a/components/Theme.tsx b/components/Theme.tsx index 78729d3c..f9d8b4bc 100644 --- a/components/Theme.tsx +++ b/components/Theme.tsx @@ -34,6 +34,8 @@ export const PALETTE_NAMES = [ "--text", "--form-invalid", + "--warning-background", + "--warning-text", "--input-background", "--input-placeholder-text", diff --git a/components/WarningHeader.module.css b/components/WarningHeader.module.css new file mode 100644 index 00000000..277d3dac --- /dev/null +++ b/components/WarningHeader.module.css @@ -0,0 +1,12 @@ +.warning{ + background-color: var(--warning-background); + padding: calc(6rem / 16); + color: var(--warning-text); + font-size: calc(16rem / 16); + text-align: center; + opacity: 1; + + /* The following are for a smooth fade in if there ever is a loading required for the warning, is not needed currently */ + /* max-height: 500px; + /* transition: max-height 1000ms ease-in, padding 100ms ease-in; */ +} \ No newline at end of file diff --git a/components/WarningHeader.tsx b/components/WarningHeader.tsx new file mode 100644 index 00000000..c8b0b632 --- /dev/null +++ b/components/WarningHeader.tsx @@ -0,0 +1,60 @@ +import { parse } from "date-fns"; +import React from "react"; + +import warnings from "../content/warnings/warnings.json"; +import { DATE_FORMAT, getLocalDateFromEST } from "../utils"; + +import styles from "./WarningHeader.module.css"; + +interface Warning { + message: string; + startDate: string; + endDate: string; +} + +function getCurrentWarning(): Warning | null { + const today = new Date(); + const currentWarnings: Warning[] = warnings.filter((warning) => { + // convert dates to date objects in EST time zone + let startDate = parse(warning.startDate, DATE_FORMAT, new Date()); + let endDate = parse(warning.endDate, DATE_FORMAT, new Date()); + + if ( + !startDate || + !endDate || + isNaN(startDate.getTime()) || // this checks if the parsed date is not valid (eg. wrong format), since getLocalDateFromEST fails with invalid dates + isNaN(endDate.getTime()) + ) { + throw new Error('WARNING WITH INVALID DATES: "' + warning.message + '"'); + } + + startDate = getLocalDateFromEST(startDate); + endDate = getLocalDateFromEST(endDate); + + return ( + startDate.getTime() <= today.getTime() && + endDate.getTime() >= today.getTime() + ); + }); + + if (currentWarnings.length > 1) { + // If more than one warning is scheduled, log an error to the console. We cannot throw an error, since the site would go down on the live + // website, on the day when more than one warning is scheduled. + console.error( + "ERROR: MORE THAN ONE WARNING SCHEDULED CURRENTLY! ", + currentWarnings + ); + } + + return currentWarnings.length === 0 ? null : currentWarnings[0]; +} + +export function WarningHeader() { + const warning = getCurrentWarning(); + + if (warning == null) { + return null; + } + + return
{warning.message}
; +} diff --git a/content/get-involved.mdx b/content/get-involved.mdx index d0d4258a..61c2aa50 100644 --- a/content/get-involved.mdx +++ b/content/get-involved.mdx @@ -38,8 +38,7 @@ University of Waterloo email address with the following: 3. your acknowledgement of having read, understood, and agreeing with our [Machine Usage Agreement](/resources/machine-usage-agreement). -~~You will need to pay the membership fee of $2 through PayPal. -A one-term payment via PayPal comes out to $2.37 (due to PayPal fees).~~ + MathSoc has waived membership fees for the Winter 2022 term, so just send syscom an email and we'll be happy to register your CSC account for free this term. @@ -48,17 +47,19 @@ an email and we'll be happy to register your CSC account for free this term. **Membership renewals for the Winter 2022 term are free.** +**Note: we no longer use Paypal to process memberships.** + For all other terms... -

+ -Use the PayPal link above to renew your membership for as many terms +Contact syscom to renew your membership for as many terms as you wish. You do not need to send us your WatCard or sign the usage agreement again. diff --git a/content/warnings/warnings.json b/content/warnings/warnings.json new file mode 100644 index 00000000..da46247d --- /dev/null +++ b/content/warnings/warnings.json @@ -0,0 +1,12 @@ +[ + { + "startDate": "February 15 2022 00:00", + "endDate": "February 20 2022 18:00", + "message": "Warning: There will be a scheduled system maintenance on February 17 from 9pm to 12pm EST" + }, + { + "startDate": "January 29 2022 21:00", + "endDate": "January 30 2022 18:00", + "message": "This is a sample warning" + } +] \ No newline at end of file diff --git a/images/team/SeshanRavikumar.jpg b/images/team/SeshanRavikumar.jpg new file mode 100644 index 00000000..b5a96f2d Binary files /dev/null and b/images/team/SeshanRavikumar.jpg differ diff --git a/lib/events.ts b/lib/events.ts index 06952eb3..76432930 100644 --- a/lib/events.ts +++ b/lib/events.ts @@ -2,14 +2,19 @@ import fs from "fs/promises"; import path from "path"; import { parse } from "date-fns"; -import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; import matter from "gray-matter"; import { MDXRemoteSerializeResult } from "next-mdx-remote"; import { serialize } from "next-mdx-remote/serialize"; import type { Props } from "../pages/events/[year]/[term]/index"; // do not use alias "@/utils" as generate-calendar imports a function from this file and ts-node is not compatible -import { Term, TERMS, isTerm } from "../utils"; +import { + Term, + TERMS, + isTerm, + DATE_FORMAT, + getLocalDateFromEST, +} from "../utils"; const EVENTS_PATH = path.join("content", "events"); @@ -55,8 +60,6 @@ export interface Event { metadata: Metadata; } -export const DATE_FORMAT = "MMMM dd yyyy HH:mm"; - export async function getEventBySlug( year: string, term: Term, @@ -284,12 +287,3 @@ function getFutureTerm(year: string, term: Term): { year: string; term: Term } { term: TERMS[index + 1], }; } - -// The date that's returned should be in local time -export function getLocalDateFromEST(date: Date) { - return utcToZonedTime( - // The parsed date is in EST - zonedTimeToUtc(date, "America/Toronto"), - Intl.DateTimeFormat().resolvedOptions().timeZone - ); -} diff --git a/lib/news.ts b/lib/news.ts index 6abd9547..1f3769a5 100644 --- a/lib/news.ts +++ b/lib/news.ts @@ -3,18 +3,20 @@ import path from "path"; import { parse } from "date-fns"; import matter from "gray-matter"; +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 "./events"; +import { DATE_FORMAT, getLocalDateFromEST } from "../utils"; export const NEWS_PATH = path.join("content", "news"); export interface Metadata { author: string; date: string; + permalink: string; } export interface News { @@ -40,6 +42,15 @@ export async function getNewsTermsByYear(year: string): Promise { .sort((a, b) => TERMS.indexOf(a) - TERMS.indexOf(b)); } +export async function getNewsDateByTerm( + year: string, + term: Term +): Promise { + return (await getNewsByTerm(year, term)).map( + (news) => news.split("-").slice(0, 3).join("-") // retrieves date from filename + ); +} + export async function getNewsByTerm( year: string, term: Term @@ -56,13 +67,21 @@ export async function getNewsByTerm( export async function getNewsBySlug( year: string, term: Term, - slug: string + slug: string, + shortened = false ): Promise { const raw = await fs.readFile( path.join(NEWS_PATH, year, term, `${slug}.md`), "utf-8" ); - const { content, data: metadata } = matter(raw); + const { content: rawContent, data: metadata } = matter(raw); + const slugDate = slug.split("-").slice(0, 3).join("-"); + const content: string = shortened + ? truncateMarkdown(rawContent, { + limit: 150, + ellipsis: true, + }) + : rawContent; return { content: await serialize(content), @@ -71,6 +90,7 @@ export async function getNewsBySlug( date: getLocalDateFromEST( parse(metadata.date, DATE_FORMAT, new Date()) ).toString(), + permalink: `/news/${year}/${term}/${slugDate}`, } as Metadata, }; } @@ -91,7 +111,9 @@ export async function getRecentNews(): Promise { try { const newsInTerm = await getNewsByTerm(year, TERMS[term]); return await Promise.all( - newsInTerm.map((slug) => getNewsBySlug(year, TERMS[term], slug)) + newsInTerm.map((slug) => { + return getNewsBySlug(year, TERMS[term], slug, true); + }) ); } catch (error) { return []; diff --git a/package-lock.json b/package-lock.json index 02ae0943..eca67f2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "fs-extra": "^10.0.0", "image-size": "^1.0.0", "ldapts": "^3.1.0", + "markdown-truncate": "^1.0.4", "next": "11.0.1", "next-mdx-remote": "3.0.4", "prettier": "^2.3.0", @@ -4719,6 +4720,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/markdown-truncate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-truncate/-/markdown-truncate-1.0.4.tgz", + "integrity": "sha512-sojm7PWqbgIfUoSVyKyyUN3glbwEgfXqL75HYvGjBHQuCkNaEHglyYt3biEIZG81H/CxhTtf2DEu4tLGWoK65Q==" + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -11576,6 +11582,11 @@ "repeat-string": "^1.0.0" } }, + "markdown-truncate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-truncate/-/markdown-truncate-1.0.4.tgz", + "integrity": "sha512-sojm7PWqbgIfUoSVyKyyUN3glbwEgfXqL75HYvGjBHQuCkNaEHglyYt3biEIZG81H/CxhTtf2DEu4tLGWoK65Q==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/package.json b/package.json index 1050c9f3..cbb202ab 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,10 @@ "@squoosh/lib": "^0.4.0", "date-fns": "^2.11.1", "date-fns-tz": "^1.1.6", - "ldapts": "^3.1.0", "fs-extra": "^10.0.0", "image-size": "^1.0.0", + "ldapts": "^3.1.0", + "markdown-truncate": "^1.0.4", "next": "11.0.1", "next-mdx-remote": "3.0.4", "prettier": "^2.3.0", diff --git a/pages/_app.css b/pages/_app.css index 92507f49..a1cfdb86 100644 --- a/pages/_app.css +++ b/pages/_app.css @@ -22,6 +22,8 @@ body { --text: #000000; --form-invalid: #9f616a; + --warning-background: #dd0014; + --warning-text: #ffffff; --input-background: #f0f0f0; --input-placeholder-text: #bbbbbb; diff --git a/pages/_app.tsx b/pages/_app.tsx index e33deb74..c166ece4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -20,6 +20,7 @@ import { } from "@/components/ShapesBackground"; import { Table } from "@/components/Table"; import { ThemeProvider } from "@/components/Theme"; +import { WarningHeader } from "@/components/WarningHeader"; import styles from "./_app.module.css"; @@ -44,6 +45,7 @@ export default function App({ Component, pageProps }: AppProps): JSX.Element { }} >

+ {/* Wrapping content with a div to allow for a display: block parent */}
diff --git a/pages/index.tsx b/pages/index.tsx index e6ced0e6..dcd01727 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -20,7 +20,7 @@ import styles from "./index.module.css"; interface Props { moreEvents: boolean; // true if there are more than 2 upcoming events events: Event[]; // array of 0 - 2 events - news: News; + news: News[]; // array of 3 news items } export default function Home(props: Props) { @@ -86,14 +86,17 @@ export default function Home(props: Props) { See past news here


- { - - - - } + {props.news.length > 0 + ? props.news.map((news, idx) => ( + + + + )) + : null}
@@ -116,7 +119,7 @@ export const getStaticProps: GetStaticProps = async () => { props: { moreEvents: upcomingEvents.length > 2, events: upcomingEvents.slice(0, 2), - news: recentNews[0], + news: recentNews.slice(0, 3), }, }; }; diff --git a/pages/news/[year]/[term]/[date].module.css b/pages/news/[year]/[term]/[date].module.css new file mode 100644 index 00000000..e752a424 --- /dev/null +++ b/pages/news/[year]/[term]/[date].module.css @@ -0,0 +1,8 @@ +.page { + padding-bottom: calc(30rem / 16); +} + +.page > h1 { + padding-bottom: calc(16rem / 16); + border-bottom: calc(1rem / 16) solid var(--primary-heading); +} diff --git a/pages/news/[year]/[term]/[date].tsx b/pages/news/[year]/[term]/[date].tsx new file mode 100644 index 00000000..65fa47a1 --- /dev/null +++ b/pages/news/[year]/[term]/[date].tsx @@ -0,0 +1,108 @@ +import { ParsedUrlQuery } from "querystring"; + +import { GetStaticPaths, GetStaticProps } from "next"; +import { MDXRemote } from "next-mdx-remote"; +import React from "react"; + +import { NewsCard } from "@/components/NewsCard"; +import { + ShapesConfig, + defaultGetShapesConfig, + GetShapesConfig, +} from "@/components/ShapesBackground"; +import { Title } from "@/components/Title"; +import { + getNewsBySlug, + getNewsByTerm, + getNewsTermsByYear, + getNewsDateByTerm, + getNewsYears, + News, +} from "@/lib/news"; +import { Term } from "@/utils"; + +import styles from "./[date].module.css"; + +interface Props { + year: string; + term: Term; + news: News[]; +} + +export default function DateNews({ news }: Props) { + const date = new Date(news[0].metadata.date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + return ( +
+ {["News", `${date}`]} +

News: {date}

+ {news.map(({ content, metadata }, idx) => ( + + + + ))} +
+ ); +} + +DateNews.getShapesConfig = ((width, height) => { + return window.innerWidth <= 768 + ? ({} as ShapesConfig) + : defaultGetShapesConfig(width, height); +}) as GetShapesConfig; + +export const getStaticProps: GetStaticProps = async ( + context +) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { year, term, date } = context.params!; + const slugs = (await getNewsByTerm(year, term)).filter((slug) => + slug.includes(date) + ); + + const news = await Promise.all( + slugs.map((slug) => getNewsBySlug(year, term, slug)) + ); + // Reverse so that we are displaying the most recent news + // of term first + return { props: { year, term, news: news.reverse() } }; +}; + +interface Params extends ParsedUrlQuery { + year: string; + term: Term; + date: string; +} + +export const getStaticPaths: GetStaticPaths = async () => { + const years = await getNewsYears(); + const terms = await Promise.all( + years.map(async (year) => { + const termsInYear = await getNewsTermsByYear(year); + return termsInYear.map((term) => ({ year, term })); + }) + ); + const dates = await Promise.all( + terms.map(async (termInYear) => { + const datesInTerm: Params[] = []; + for (const { year, term } of termInYear) { + const dates = await getNewsDateByTerm(year, term); + dates.map((date) => datesInTerm.push({ year, term, date })); + } + return datesInTerm.flat(); + }) + ); + + return { + paths: dates.flat().map((params) => ({ params })), + fallback: false, + }; +}; diff --git a/scripts/change-dates.ts b/scripts/change-dates.ts index a4a112d4..a6dacef6 100644 --- a/scripts/change-dates.ts +++ b/scripts/change-dates.ts @@ -3,8 +3,9 @@ import path from "path"; import { format } from "date-fns"; +import { DATE_FORMAT } from "@/utils"; + import { - DATE_FORMAT, getEventsByTerm, getEventTermsByYear, getEventYears, diff --git a/types.d.ts b/types.d.ts index 1a8e2879..17bdcfd8 100644 --- a/types.d.ts +++ b/types.d.ts @@ -16,3 +16,10 @@ declare module "*.md" { export default ReactComponent; } + +declare module "markdown-truncate" { + export default function truncateMarkdown( + inputText: string, + options: { limit: number; ellipsis: boolean } + ): string; +} diff --git a/utils.ts b/utils.ts index fc08499a..0df41524 100644 --- a/utils.ts +++ b/utils.ts @@ -1,5 +1,8 @@ +import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; + export const TERMS = ["winter", "spring", "fall"] as const; export type Term = typeof TERMS[number]; +export const DATE_FORMAT = "MMMM dd yyyy HH:mm"; // https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates export function isTerm(x: string): x is Term { @@ -9,3 +12,12 @@ export function isTerm(x: string): x is Term { export function capitalize(str: string) { return str.slice(0, 1).toUpperCase() + str.slice(1); } + +// Converts a date to local time +export function getLocalDateFromEST(date: Date): Date { + return utcToZonedTime( + // The date parameter is in EST + zonedTimeToUtc(date, "America/Toronto"), + Intl.DateTimeFormat().resolvedOptions().timeZone + ); +}