Merge branch 'main' into b72zhou-ldap-exec
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Beihao Zhou 2022-02-27 09:19:25 -05:00
commit 83b68a3ce3
20 changed files with 304 additions and 35 deletions

View File

@ -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 {

View File

@ -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<NewsCardProps> = ({
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<NewsCardProps> = ({
</h1>
<address className={styles.author}>{author}</address>
<div className={styles.content}>{children}</div>
{!fit && (
<Link href={permalink}>
<span>Learn more</span>
</Link>
)}
</article>
);
};

View File

@ -34,6 +34,8 @@ export const PALETTE_NAMES = [
"--text",
"--form-invalid",
"--warning-background",
"--warning-text",
"--input-background",
"--input-placeholder-text",

View File

@ -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; */
}

View File

@ -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 <div className={styles.warning}>{warning.message}</div>;
}

View File

@ -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).~~
<!--~~You will need to pay the membership fee of $2 through WUSA store.~~-->
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...
<p>
<!--<p>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_s-xclick"/>
<input type="hidden" name="hosted_button_id" value="9065852"/>
<button size="small" name="submit">Renew by PayPal</button>
</form>
</p>
</p>-->
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.

View File

@ -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"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

View File

@ -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
);
}

View File

@ -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<Term[]> {
.sort((a, b) => TERMS.indexOf(a) - TERMS.indexOf(b));
}
export async function getNewsDateByTerm(
year: string,
term: Term
): Promise<string[]> {
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<News> {
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<News[]> {
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 [];

11
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -22,6 +22,8 @@ body {
--text: #000000;
--form-invalid: #9f616a;
--warning-background: #dd0014;
--warning-text: #ffffff;
--input-background: #f0f0f0;
--input-placeholder-text: #bbbbbb;

View File

@ -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 {
}}
>
<div className={styles.appContainer}>
<WarningHeader />
<Navbar />
{/* Wrapping content with a div to allow for a display: block parent */}
<div className={styles.contentContainer}>

View File

@ -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 <Link href="/news/archive">here</Link>
</p>
<hr className={styles.cardsDividingLine} />
{
{props.news.length > 0
? props.news.map((news, idx) => (
<NewsCard
{...props.news.metadata}
date={new Date(props.news.metadata.date)}
{...news.metadata}
date={new Date(news.metadata.date)}
key={`${news.metadata.date.toString()}${idx}`}
>
<MDXRemote {...props.news.content} />
<MDXRemote {...news.content} />
</NewsCard>
}
))
: null}
</section>
</div>
</div>
@ -116,7 +119,7 @@ export const getStaticProps: GetStaticProps<Props> = async () => {
props: {
moreEvents: upcomingEvents.length > 2,
events: upcomingEvents.slice(0, 2),
news: recentNews[0],
news: recentNews.slice(0, 3),
},
};
};

View File

@ -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);
}

View File

@ -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 (
<div className={styles.page}>
<Title>{["News", `${date}`]}</Title>
<h1>News: {date}</h1>
{news.map(({ content, metadata }, idx) => (
<NewsCard
key={idx}
{...metadata}
date={new Date(metadata.date)}
fit={true}
>
<MDXRemote {...content} />
</NewsCard>
))}
</div>
);
}
DateNews.getShapesConfig = ((width, height) => {
return window.innerWidth <= 768
? ({} as ShapesConfig)
: defaultGetShapesConfig(width, height);
}) as GetShapesConfig;
export const getStaticProps: GetStaticProps<Props, Params> = 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<Params> = 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,
};
};

View File

@ -3,8 +3,9 @@ import path from "path";
import { format } from "date-fns";
import { DATE_FORMAT } from "@/utils";
import {
DATE_FORMAT,
getEventsByTerm,
getEventTermsByYear,
getEventYears,

7
types.d.ts vendored
View File

@ -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;
}

View File

@ -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
);
}