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/lib/news.ts b/lib/news.ts index 69812ec5..1f3769a5 100644 --- a/lib/news.ts +++ b/lib/news.ts @@ -3,6 +3,7 @@ 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"; @@ -15,6 +16,7 @@ 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/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/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; +}