Add Multiple News Items on Homepage and Single News Item Page (Closes #281) #390
|
@ -3,6 +3,7 @@
|
||||||
max-width: calc(524rem / 16);
|
max-width: calc(524rem / 16);
|
||||||
background-color: var(--primary-background);
|
background-color: var(--primary-background);
|
||||||
border-radius: calc(20rem / 16);
|
border-radius: calc(20rem / 16);
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fit.card {
|
.fit.card {
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Link } from "./Link";
|
||||||
|
|
||||||
import styles from "./NewsCard.module.css";
|
import styles from "./NewsCard.module.css";
|
||||||
|
|
||||||
interface NewsCardProps {
|
interface NewsCardProps {
|
||||||
date: Date;
|
date: Date;
|
||||||
author: string;
|
author: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
permalink: string;
|
||||||
fit?: boolean;
|
fit?: boolean;
|
||||||
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +16,8 @@ export const NewsCard: React.FC<NewsCardProps> = ({
|
||||||
date,
|
date,
|
||||||
author,
|
author,
|
||||||
children,
|
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];
|
const classes = fit ? [styles.card, styles.fit] : [styles.card];
|
||||||
|
|
||||||
|
@ -30,6 +34,11 @@ export const NewsCard: React.FC<NewsCardProps> = ({
|
||||||
</h1>
|
</h1>
|
||||||
<address className={styles.author}>{author}</address>
|
<address className={styles.author}>{author}</address>
|
||||||
<div className={styles.content}>{children}</div>
|
<div className={styles.content}>{children}</div>
|
||||||
|
{!fit && (
|
||||||
|
<Link href={permalink}>
|
||||||
|
<span>Learn more</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
28
lib/news.ts
|
@ -3,6 +3,7 @@ import path from "path";
|
||||||
|
|
||||||
import { parse } from "date-fns";
|
import { parse } from "date-fns";
|
||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
|
import truncateMarkdown from "markdown-truncate";
|
||||||
import { MDXRemoteSerializeResult } from "next-mdx-remote";
|
import { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||||
import { serialize } from "next-mdx-remote/serialize";
|
import { serialize } from "next-mdx-remote/serialize";
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ export const NEWS_PATH = path.join("content", "news");
|
||||||
export interface Metadata {
|
export interface Metadata {
|
||||||
author: string;
|
author: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
permalink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface News {
|
export interface News {
|
||||||
|
@ -40,6 +42,15 @@ export async function getNewsTermsByYear(year: string): Promise<Term[]> {
|
||||||
.sort((a, b) => TERMS.indexOf(a) - TERMS.indexOf(b));
|
.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(
|
export async function getNewsByTerm(
|
||||||
year: string,
|
year: string,
|
||||||
term: Term
|
term: Term
|
||||||
|
@ -56,13 +67,21 @@ export async function getNewsByTerm(
|
||||||
export async function getNewsBySlug(
|
export async function getNewsBySlug(
|
||||||
year: string,
|
year: string,
|
||||||
term: Term,
|
term: Term,
|
||||||
slug: string
|
slug: string,
|
||||||
|
shortened = false
|
||||||
): Promise<News> {
|
): Promise<News> {
|
||||||
const raw = await fs.readFile(
|
const raw = await fs.readFile(
|
||||||
path.join(NEWS_PATH, year, term, `${slug}.md`),
|
path.join(NEWS_PATH, year, term, `${slug}.md`),
|
||||||
"utf-8"
|
"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 {
|
return {
|
||||||
content: await serialize(content),
|
content: await serialize(content),
|
||||||
|
@ -71,6 +90,7 @@ export async function getNewsBySlug(
|
||||||
date: getLocalDateFromEST(
|
date: getLocalDateFromEST(
|
||||||
parse(metadata.date, DATE_FORMAT, new Date())
|
parse(metadata.date, DATE_FORMAT, new Date())
|
||||||
).toString(),
|
).toString(),
|
||||||
|
permalink: `/news/${year}/${term}/${slugDate}`,
|
||||||
} as Metadata,
|
} as Metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -91,7 +111,9 @@ export async function getRecentNews(): Promise<News[]> {
|
||||||
try {
|
try {
|
||||||
const newsInTerm = await getNewsByTerm(year, TERMS[term]);
|
const newsInTerm = await getNewsByTerm(year, TERMS[term]);
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
newsInTerm.map((slug) => getNewsBySlug(year, TERMS[term], slug))
|
newsInTerm.map((slug) => {
|
||||||
|
return getNewsBySlug(year, TERMS[term], slug, true);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"image-size": "^1.0.0",
|
"image-size": "^1.0.0",
|
||||||
"ldapts": "^3.1.0",
|
"ldapts": "^3.1.0",
|
||||||
|
"markdown-truncate": "^1.0.4",
|
||||||
"next": "11.0.1",
|
"next": "11.0.1",
|
||||||
"next-mdx-remote": "3.0.4",
|
"next-mdx-remote": "3.0.4",
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
|
@ -4719,6 +4720,11 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/md5.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||||
|
@ -11576,6 +11582,11 @@
|
||||||
"repeat-string": "^1.0.0"
|
"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": {
|
"md5.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||||
|
|
|
@ -24,9 +24,10 @@
|
||||||
"@squoosh/lib": "^0.4.0",
|
"@squoosh/lib": "^0.4.0",
|
||||||
"date-fns": "^2.11.1",
|
"date-fns": "^2.11.1",
|
||||||
"date-fns-tz": "^1.1.6",
|
"date-fns-tz": "^1.1.6",
|
||||||
"ldapts": "^3.1.0",
|
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"image-size": "^1.0.0",
|
"image-size": "^1.0.0",
|
||||||
|
"ldapts": "^3.1.0",
|
||||||
|
"markdown-truncate": "^1.0.4",
|
||||||
"next": "11.0.1",
|
"next": "11.0.1",
|
||||||
"next-mdx-remote": "3.0.4",
|
"next-mdx-remote": "3.0.4",
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
|
|
|
@ -20,7 +20,7 @@ import styles from "./index.module.css";
|
||||||
interface Props {
|
interface Props {
|
||||||
moreEvents: boolean; // true if there are more than 2 upcoming events
|
moreEvents: boolean; // true if there are more than 2 upcoming events
|
||||||
events: Event[]; // array of 0 - 2 events
|
events: Event[]; // array of 0 - 2 events
|
||||||
news: News;
|
news: News[]; // array of 3 news items
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home(props: Props) {
|
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>
|
See past news <Link href="/news/archive">here</Link>
|
||||||
</p>
|
</p>
|
||||||
<hr className={styles.cardsDividingLine} />
|
<hr className={styles.cardsDividingLine} />
|
||||||
{
|
{props.news.length > 0
|
||||||
|
? props.news.map((news, idx) => (
|
||||||
<NewsCard
|
<NewsCard
|
||||||
{...props.news.metadata}
|
{...news.metadata}
|
||||||
date={new Date(props.news.metadata.date)}
|
date={new Date(news.metadata.date)}
|
||||||
|
key={`${news.metadata.date.toString()}${idx}`}
|
||||||
>
|
>
|
||||||
<MDXRemote {...props.news.content} />
|
<MDXRemote {...news.content} />
|
||||||
</NewsCard>
|
</NewsCard>
|
||||||
}
|
))
|
||||||
|
: null}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -116,7 +119,7 @@ export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||||
props: {
|
props: {
|
||||||
moreEvents: upcomingEvents.length > 2,
|
moreEvents: upcomingEvents.length > 2,
|
||||||
events: upcomingEvents.slice(0, 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) => (
|
||||||
a258wang
commented
Is the Is the `span` here still necessary?
|
|||||||
|
<NewsCard
|
||||||
|
key={idx}
|
||||||
|
{...metadata}
|
||||||
|
date={new Date(metadata.date)}
|
||||||
|
fit={true}
|
||||||
|
>
|
||||||
|
<MDXRemote {...content} />
|
||||||
a258wang
commented
It looks like we're passing It looks like we're passing `fit={true}` to the `NewsCard`, however the "fit" styling is declared on the `MiniNewsCard`. Is this intentional?
e26chiu
commented
The "fit" style is also declared in The "fit" style is also declared in `NewsCard`. I just copied the code from `MiniNewsCard`. The "fit" style is still important to keep the formatting of the `NewsCard` though. Since it's a duplicate code, do you think we need to change the way we include the stylesheet or no?
a258wang
commented
Ah, I think what's happening is we're using the Ah, I think what's happening is we're using the `NewsCard` component on both the Home page and on the New Archive pages - on the Home page we don't want the "fit" styles, however on the Archive pages we want them. Perhaps we can leave a quick comment in the `NewsCard` component explaining the purpose of the "fit" styles?
|
|||||||
|
</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() } };
|
||||||
a258wang
commented
Is there a reason why we want to console log here? If it's just for debugging/testing purposes, we should probably get rid of it before merging. 🙂 Is there a reason why we want to console log here? If it's just for debugging/testing purposes, we should probably get rid of it before merging. 🙂
|
|||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -16,3 +16,10 @@ declare module "*.md" {
|
||||||
|
|
||||||
export default ReactComponent;
|
export default ReactComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "markdown-truncate" {
|
||||||
|
export default function truncateMarkdown(
|
||||||
|
inputText: string,
|
||||||
|
options: { limit: number; ellipsis: boolean }
|
||||||
|
): string;
|
||||||
|
}
|
||||||
|
|
It might be helpful to leave a comment here explaining what the
fit
prop is used for.