Merge remote-tracking branch 'origin/main' into dark-theme
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Miniapple8888 2022-02-28 16:50:31 -05:00
commit 5e25bd2db1
18 changed files with 298 additions and 30 deletions

View File

@ -3,6 +3,7 @@
max-width: calc(524rem / 16);
background-color: var(--card-background);
border-radius: calc(20rem / 16);
margin-bottom: 1rem;
}
.fit.card {
@ -32,6 +33,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

@ -43,6 +43,8 @@ export const PALETTE_NAMES = [
"--mini-event-card-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

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

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

@ -31,6 +31,8 @@ body {
--secondary-subtitle: #2a2a62;
--form-invalid: #9f616a;
--warning-background: #dd0014;
--warning-text: #ffffff;
--link: #1482e3;
--link-hover: #4ed3b2;

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} />
{
<NewsCard
{...props.news.metadata}
date={new Date(props.news.metadata.date)}
>
<MDXRemote {...props.news.content} />
</NewsCard>
}
{props.news.length > 0
? props.news.map((news, idx) => (
<NewsCard
{...news.metadata}
date={new Date(news.metadata.date)}
key={`${news.metadata.date.toString()}${idx}`}
>
<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
);
}