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." : ( + + )} + +} \ 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.altText} + ) : ( + {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