diff --git a/components/news/article-date.js b/components/news/article-date.js
new file mode 100644
index 00000000..89a66f76
--- /dev/null
+++ b/components/news/article-date.js
@@ -0,0 +1,13 @@
+import { styled } from '@mui/system'
+import { formatDate } from '@/utils/date'
+
+export const DateSpan = styled('span')({
+ fontSize: '95%',
+ fontWeight: 'bold',
+ margin: 0,
+ lineHeight: 2,
+})
+
+export const ArticleDate = ({ date }) => {
+ return { formatDate(date) }
+}
\ No newline at end of file
diff --git a/components/news/time-grouping.js b/components/news/time-grouping.js
new file mode 100644
index 00000000..e4dd442b
--- /dev/null
+++ b/components/news/time-grouping.js
@@ -0,0 +1,47 @@
+import { dateToSlug } from "@/utils/slug"
+import { Page } from "../layout"
+import { Link } from "../link"
+import { Typography } from "@mui/material"
+
+const MONTHS = [ undefined, "Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."]
+const DAYS =
+ [undefined, "1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th","9th", "10th",
+ "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", "20th",
+ "21st", "22nd", "23rd", "24th", "25th", "26th", "27th", "28th", "29th", "30th", "31st"]
+
+export const TimeGrouping = ({
+ year,
+ month,
+ day,
+ posts,
+}) => {
+ const title = `Articles during ${
+ month !== undefined ? `${MONTHS[Number(month)]} ` : ''
+ }${
+ day !== undefined ? `${DAYS[Number(day)]}, ` : ''
+ }${year}`
+
+ return
+
+ ← Back to news
+
+ {posts.length === 0 ? "No articles for this period." : (
+
+ {
+ posts.map(({ title, publishDate, slug }, i) => {
+ const d = new Date(publishDate)
+ return (
+ -
+
+ {`${d.getUTCMonth() + 1}/${d.getUTCDate()}/${d.getUTCFullYear()}`}
+ {" - "}
+ {title}
+
+
+ )
+ })
+ }
+
+ )}
+
+}
\ No newline at end of file
diff --git a/lib/strapi/index.js b/lib/strapi/index.js
index 70d04790..1806cd03 100644
--- a/lib/strapi/index.js
+++ b/lib/strapi/index.js
@@ -7,4 +7,5 @@ export * from './groupsGraphQL'
export * from './peopleGraphQL'
export * from './fetchOurWorkTrayItems'
export * from './newsAppearancesGraphQL'
-export * from './newsSWR'
\ No newline at end of file
+export * from './newsSWR'
+export * from './newsGraphQL'
\ No newline at end of file
diff --git a/lib/strapi/newsGraphQL.js b/lib/strapi/newsGraphQL.js
new file mode 100644
index 00000000..974d2dac
--- /dev/null
+++ b/lib/strapi/newsGraphQL.js
@@ -0,0 +1,118 @@
+import { fetchStrapiGraphQL } from "./fetchStrapiGraphQL";
+
+export const fetchArticle = async (slug) => {
+ const articleGql = await fetchStrapiGraphQL(`
+ fragment PersonAttributes on PersonRelationResponseCollection {
+ data {
+ attributes {
+ firstName
+ lastName
+ slug
+ }
+ }
+ }
+
+ query {
+ posts(filters: { slug: { eq: "${slug}" }}) {
+ data {
+ attributes {
+ title
+ subtitle
+ slug
+ publishDate
+ newsOrBlog
+ renciAuthors {
+ ...PersonAttributes
+ }
+ externalAuthors
+ metadata {
+ metaTitle
+ metaDescription
+ shareImage {
+ data {
+ attributes {
+ url
+ }
+ }
+ }
+ }
+ people {
+ ...PersonAttributes
+ }
+ researchGroups {
+ data {
+ attributes {
+ name
+ slug
+ }
+ }
+ }
+ collaborations {
+ data {
+ attributes {
+ name
+ slug
+ }
+ }
+ }
+ projects {
+ data {
+ attributes {
+ name
+ slug
+ }
+ }
+ }
+ organizations {
+ data {
+ attributes {
+ name
+ slug
+ }
+ }
+ }
+ tags {
+ data {
+ attributes {
+ name
+ }
+ }
+ }
+ content {
+ __typename
+ ...on ComponentPostSectionsImage {
+ caption
+ altText
+ image {
+ data {
+ attributes {
+ url
+ width
+ height
+ }
+ }
+ }
+ }
+ ...on ComponentPostSectionsRichText{
+ content
+ }
+ }
+ }
+ }
+ }
+ }
+ `);
+
+ if (articleGql?.data?.posts?.data?.length !== 1) return null;
+
+ return articleGql.data.posts.data.map(({ attributes }) => ({
+ ...attributes,
+ renciAuthors: attributes.renciAuthors.data.map(({ attributes }) => attributes),
+ people: attributes.people.data.map(({ attributes }) => ({ ...attributes, name: `${attributes.firstName} ${attributes.lastName}`})),
+ researchGroups: attributes.researchGroups.data.map(({ attributes }) => attributes),
+ collaborations: attributes.collaborations.data.map(({ attributes }) => attributes),
+ projects: attributes.projects.data.map(({ attributes }) => attributes),
+ organizations: attributes.organizations.data.map(({ attributes }) => attributes),
+ postTags: attributes.tags.data.map(({ attributes }) => attributes),
+ }))[0];
+}
\ No newline at end of file
diff --git a/pages/news/[year]/[month]/[day]/[slug].js b/pages/news/[year]/[month]/[day]/[slug].js
new file mode 100644
index 00000000..daf7fe86
--- /dev/null
+++ b/pages/news/[year]/[month]/[day]/[slug].js
@@ -0,0 +1,191 @@
+import { Fragment } from "react"
+import { Page, Section } from "@/components/layout";
+import { fetchArticle, fetchStrapiGraphQL } from "@/lib/strapi";
+import { Divider, Typography, Box, Stack } from "@mui/material";
+import { Markdown } from "@/components/markdown";
+import Image from "next/image";
+import { ArticleDate } from "@/components/news/article-date"
+import { Tag } from "@/components/news/tag"
+import qs from "qs";
+import { Link } from "@/components/link"
+
+export default function Article({ article }) {
+
+ const tags = [
+ article.projects.map((x) => ({ ...x, type: 'projects' })),
+ article.people.map((x) => ({ ...x, type: 'people' })),
+ article.collaborations.map((x) => ({ ...x, type: 'collaborations' })),
+ article.researchGroups.map((x) => ({ ...x, type: 'researchGroups' })),
+ article.organizations.map((x) => ({ ...x, type: 'organizations' })),
+ article.postTags.map((x) => ({ ...x, type: 'postTags' }))
+ ].flat();
+
+ const createTagLinkURL = (id, type) => {
+ return `/news?${qs.stringify({[type]: id})}`
+ }
+
+ return (
+
+
+ {/* Defines the article width, does not include next/previous article buttons */}
+
+
+ {/* container that holds the date and label on the same line */}
+
+
+
+
+ {article.newsOrBlog}
+
+
+
+ {/*title moved down here below the date/label line*/}
+
+ { article.title }
+
+
+ {/*Subheading/subtitle if one exists*/}
+ {
+ article.subtitle && (
+
+ {article.subtitle}
+
+ )
+ }
+
+ {tags.map(({ name, slug, type }, i) => {
+ const id = type === 'postTags' ? name : slug;
+
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+
+ {/*Article content is mapped over because each section is grouped by content type, separating rich text from images*/}
+ {
+ article.content.map((item)=> {
+ return item.__typename == "ComponentPostSectionsImage" ? (
+
+ ) : (
+ {item.content}
+ )
+ })
+ }
+
+
+
+
+
+
+ {article.researchGroups[0] && (
+
+ Research Groups:
+
+ {
+ article.researchGroups.map((item, i) => (
+ - {item.name}
+ ))
+ }
+
+
+
+ )}
+ {article.collaborations[0] && (
+
+ Collaborations:
+
+ {
+ article.collaborations.map((item, i) => (
+ - {item.name}
+ ))
+ }
+
+
+
+ )}
+ {article.projects[0] && (
+
+ Projects:
+ {
+ article.projects.map((item, i) => (
+ {item.name}
+ ))
+ }
+
+
+ )}
+ {article.people[0] && (
+
+ People:
+
+ {
+ article.people.map((item, i) => (
+ - {item.name}
+ ))
+ }
+
+
+
+ )}
+
+
+ )
+}
+
+export async function getStaticPaths() {
+ const postsGql = await fetchStrapiGraphQL(`query {
+ posts(pagination: { limit: 1000 }, sort: "publishDate:desc") {
+ data {
+ attributes {
+ slug
+ publishDate
+ }
+ }
+ }
+ }`);
+
+ const paths = postsGql.data.posts.data.map(({ attributes: { publishDate, slug } }) => {
+ const date = new Date(publishDate);
+
+ return {
+ params: {
+ year: date.getUTCFullYear().toString(),
+ month: (date.getUTCMonth() + 1).toString(),
+ day: date.getUTCDate().toString(),
+ slug,
+ }
+ }
+ });
+
+ return {
+ paths,
+ fallback: 'blocking',
+ };
+}
+
+export async function getStaticProps({ params: { slug } }) {
+ const article = await fetchArticle(slug);
+ if (article === null || article.length) return { notFound: true };
+ return { props: { article } }
+}
diff --git a/pages/news/[year]/[month]/[day]/index.js b/pages/news/[year]/[month]/[day]/index.js
new file mode 100644
index 00000000..2212edea
--- /dev/null
+++ b/pages/news/[year]/[month]/[day]/index.js
@@ -0,0 +1,86 @@
+import { TimeGrouping } from "@/components/news/time-grouping";
+import { fetchStrapiGraphQL } from "@/lib/strapi";
+import { isValidDate } from "@/utils/date";
+
+export default function MonthCatalog({ year, month, day, posts }) {
+ return
+}
+
+export async function getStaticPaths() {
+ const postsGql = await fetchStrapiGraphQL(`query {
+ posts(pagination: { limit: 1000 }, sort: "publishDate:desc") {
+ data {
+ attributes {
+ publishDate
+ }
+ }
+ }
+ }`);
+
+ // we need to create a list of paths for each year/month/day group. In order to avoid duplicates,
+ // here we create a Set of JSON.stringify({ year, month, day }), which can then be cast to an array and parsed
+ const yearSet = new Set(postsGql.data.posts.data.map(({ attributes: { publishDate } }) => {
+ const date = new Date(publishDate);
+
+ return JSON.stringify({
+ year: date.getUTCFullYear().toString(),
+ month: (date.getUTCMonth() + 1).toString(),
+ day: date.getUTCDate().toString(),
+ })
+ }));
+
+ const paths = Array.from(yearSet).map((date) => ({ params: JSON.parse(date) }));
+
+ return {
+ paths,
+ fallback: 'blocking',
+ };
+}
+
+export async function getStaticProps({ params }) {
+
+ const date = new Date(params.year, params.month - 1, params.day);
+
+ if (!isValidDate(date)) return {
+ notFound: true,
+ }
+
+ const dateStr = date.toISOString().split('T')[0];
+
+ const postsGql = await fetchStrapiGraphQL(`
+ query {
+ posts(
+ sort: "publishDate:desc"
+ filters: { publishDate: { eq: "${dateStr}" } }
+ pagination: { limit: 1000 }
+ ) {
+ data {
+ attributes {
+ title
+ publishDate
+ slug
+ }
+ }
+ }
+ }
+ `);
+
+ console.log(dateStr);
+ console.log(postsGql);
+
+ const posts = postsGql.data.posts.data.map(({ attributes }) => attributes);
+
+ return {
+ props: {
+ year: params.year,
+ month: params.month,
+ day: params.day,
+ posts
+ }
+ }
+}
diff --git a/pages/news/[year]/[month]/index.js b/pages/news/[year]/[month]/index.js
new file mode 100644
index 00000000..c486df72
--- /dev/null
+++ b/pages/news/[year]/[month]/index.js
@@ -0,0 +1,82 @@
+import { TimeGrouping } from "@/components/news/time-grouping";
+import { fetchStrapiGraphQL } from "@/lib/strapi";
+import { isValidDate } from "@/utils/date";
+
+export default function MonthCatalog({ year, month, posts }) {
+ return
+}
+
+export async function getStaticPaths() {
+ const postsGql = await fetchStrapiGraphQL(`query {
+ posts(pagination: { limit: 1000 }, sort: "publishDate:desc") {
+ data {
+ attributes {
+ publishDate
+ }
+ }
+ }
+ }`);
+
+ // we need to create a list of paths for each year/month pair. In order to avoid duplicates,
+ // here we create a Set of JSON.stringify({ year, month }), which can then be cast to an array and parsed
+ const yearSet = new Set(postsGql.data.posts.data.map(({ attributes: { publishDate } }) => {
+ const date = new Date(publishDate);
+
+ return JSON.stringify({
+ year: date.getUTCFullYear().toString(),
+ month: (date.getUTCMonth() + 1).toString(),
+ })
+ }));
+
+ const paths = Array.from(yearSet).map((yearAndMonth) => ({ params: JSON.parse(yearAndMonth) }));
+
+ return {
+ paths,
+ fallback: 'blocking',
+ };
+}
+
+export async function getStaticProps({ params }) {
+
+ const firstDayOfMonth = new Date(params.year, params.month - 1, 1);
+ const lastDayOfMonth = new Date(params.year, params.month - 1 + 1, 0); // 0th day of the next month, i.e. last day of this month
+
+ if (!isValidDate(firstDayOfMonth) || !isValidDate(lastDayOfMonth)) return {
+ notFound: true,
+ }
+
+ const firstDayOfMonthStr = firstDayOfMonth.toISOString().split('T')[0];
+ const lastDayOfMonthStr = lastDayOfMonth.toISOString().split('T')[0];
+
+ const postsGql = await fetchStrapiGraphQL(`
+ query {
+ posts(
+ sort: "publishDate:desc"
+ filters: { publishDate: { between: ["${firstDayOfMonthStr}", "${lastDayOfMonthStr}"] } }
+ pagination: { limit: 1000 }
+ ) {
+ data {
+ attributes {
+ title
+ publishDate
+ slug
+ }
+ }
+ }
+ }
+ `);
+
+ const posts = postsGql.data.posts.data.map(({ attributes }) => attributes);
+
+ return {
+ props: {
+ year: params.year,
+ month: params.month,
+ posts
+ }
+ }
+}
diff --git a/pages/news/[year]/index.js b/pages/news/[year]/index.js
new file mode 100644
index 00000000..ddae6daf
--- /dev/null
+++ b/pages/news/[year]/index.js
@@ -0,0 +1,72 @@
+import { TimeGrouping } from "@/components/news/time-grouping";
+import { fetchStrapiGraphQL } from "@/lib/strapi";
+import { isValidDate } from "@/utils/date";
+
+export default function YearCatalog({ year, posts }) {
+ return
+}
+
+export async function getStaticPaths() {
+ const postsGql = await fetchStrapiGraphQL(`query {
+ posts(pagination: { limit: 1000 }, sort: "publishDate:desc") {
+ data {
+ attributes {
+ publishDate
+ }
+ }
+ }
+ }`);
+
+ const yearSet = new Set(postsGql.data.posts.data.map(({ attributes: { publishDate } }) => (
+ new Date(publishDate).getUTCFullYear().toString()
+ )));
+
+ const paths = Array.from(yearSet).map((year) => ({ params: { year } }));
+
+ return {
+ paths,
+ fallback: 'blocking',
+ };
+}
+
+export async function getStaticProps({ params }) {
+ const firstDayOfYear = new Date(params.year, 0, 1);
+ const lastDayOfYear = new Date(params.year, 11, 31);
+
+ if (!isValidDate(firstDayOfYear) || !isValidDate(lastDayOfYear)) return {
+ notFound: true,
+ }
+
+ const firstDayOfYearStr = firstDayOfYear.toISOString().split('T')[0];
+ const lastDayOfYearStr = lastDayOfYear.toISOString().split('T')[0];
+
+ const postsGql = await fetchStrapiGraphQL(`
+ query {
+ posts(
+ sort: "publishDate:desc"
+ filters: { publishDate: { between: ["${firstDayOfYearStr}", "${lastDayOfYearStr}"] } }
+ pagination: { limit: 1000 }
+ ) {
+ data {
+ attributes {
+ title
+ publishDate
+ slug
+ }
+ }
+ }
+ }
+ `);
+
+ const posts = postsGql.data.posts.data.map(({ attributes }) => attributes);
+
+ return {
+ props: {
+ year: params.year,
+ posts
+ }
+ }
+}
diff --git a/style/theme.js b/style/theme.js
index 3db7fad7..d6e84537 100644
--- a/style/theme.js
+++ b/style/theme.js
@@ -32,6 +32,7 @@ const typography = {
},
h3: {
fontSize: 'clamp(1.3rem, 0.986rem + 0.571vw, 1.7rem)',
+ paddingBottom: '0.5rem'
},
h4: {
fontSize: 'clamp(1.2rem, 3vw, 1.6rem)',
diff --git a/utils/date.js b/utils/date.js
new file mode 100644
index 00000000..1530bbdc
--- /dev/null
+++ b/utils/date.js
@@ -0,0 +1,14 @@
+/**
+ * Checks if a JS Date object is valid
+ */
+export const isValidDate = (d) => {
+ if (!(d instanceof Date)) return false;
+ return d.toUTCString() !== "Invalid Date"
+}
+
+const dateOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
+
+export const formatDate = dateString => {
+ const date = new Date(dateString)
+ return date.toLocaleDateString('en-US', dateOptions)
+}
\ No newline at end of file
diff --git a/utils/slug.js b/utils/slug.js
new file mode 100644
index 00000000..91c34a8f
--- /dev/null
+++ b/utils/slug.js
@@ -0,0 +1,15 @@
+/**
+ * create a slug string from a date
+ * @example ```js
+ * slugFromDate(new Date("2023-02-01")) // "2023/02/01"
+ * ```
+ */
+export const dateToSlug = (d) => {
+ const date = new Date(d)
+ const [day, month, year] = [
+ date.getUTCDate(),
+ date.getUTCMonth() + 1,
+ date.getUTCFullYear(),
+ ]
+ return `${year}/${month}/${day}`;
+}
\ No newline at end of file