Merge branch 'main' into b72zhou-ldap-exec
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
commit
83b68a3ce3
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -34,6 +34,8 @@ export const PALETTE_NAMES = [
|
|||
"--text",
|
||||
|
||||
"--form-invalid",
|
||||
"--warning-background",
|
||||
"--warning-text",
|
||||
|
||||
"--input-background",
|
||||
"--input-placeholder-text",
|
||||
|
|
|
@ -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; */
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
30
lib/news.ts
30
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<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 [];
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -22,6 +22,8 @@ body {
|
|||
--text: #000000;
|
||||
|
||||
--form-invalid: #9f616a;
|
||||
--warning-background: #dd0014;
|
||||
--warning-text: #ffffff;
|
||||
|
||||
--input-background: #f0f0f0;
|
||||
--input-placeholder-text: #bbbbbb;
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -3,8 +3,9 @@ import path from "path";
|
|||
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { DATE_FORMAT } from "@/utils";
|
||||
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getEventsByTerm,
|
||||
getEventTermsByYear,
|
||||
getEventYears,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
12
utils.ts
12
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
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue