diff --git a/README.md b/README.md index bfbcbc14..a60bfa81 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # renci-dot-org +## Project deployment overview +*Note that this diagram is for the entire project, while this repo represents the Next.js application exclusively.* +![image](https://github.com/mbwatson/renci-dot-org/assets/16181779/26d297d4-867d-4cdc-90b8-6ad3088a3b14) + This is a [Next.js](https://nextjs.org/) project. To run the development server: ```bash @@ -44,4 +48,4 @@ If it already exists, you can update the secret by editing the `token` field in ```bash kubectl edit secret renci-dot-org-api ``` -Note that is it needs to be base64 encoded, so translate the token with `echo "YOUR_TOKEN" | base64` before copying it into the yaml file. \ No newline at end of file +Note that is it needs to be base64 encoded, so translate the token with `echo "YOUR_TOKEN" | base64` before copying it into the yaml file. diff --git a/components/layout/menu/our-work-tray.js b/components/layout/menu/our-work-tray.js index 23be991a..063d59c3 100644 --- a/components/layout/menu/our-work-tray.js +++ b/components/layout/menu/our-work-tray.js @@ -1,4 +1,4 @@ -import { Container, Fade, Grid, List, ListItem, ListSubheader, Paper, Typography } from '@mui/material' +import { Container, Fade, Grid, List, ListItem, ListSubheader, Paper, Divider, Typography } from '@mui/material' import style from './menu.module.css' import { useConfig } from '../../../context' import { Link } from '../../' @@ -53,6 +53,11 @@ export const OurWorkTray = ({ )) } + {/* Add a one-off menu item that links to the list of all RENCI projects */} + + + See All Projects + diff --git a/components/layout/section/section.js b/components/layout/section/section.js index 236eabee..3d260e44 100644 --- a/components/layout/section/section.js +++ b/components/layout/section/section.js @@ -29,7 +29,7 @@ export const Section = ({ title, children }) => { ) } - + { children } diff --git a/components/news/article-preview.js b/components/news/article-preview.js index 98d57130..46725c7e 100644 --- a/components/news/article-preview.js +++ b/components/news/article-preview.js @@ -84,7 +84,6 @@ export const ArticlePreview = ({ } - Read more → +} + +export const HomePageArticlePreview = ({article }) => { + const date = new Date(article.publishDate) + const [day, month, year] = [ + date.getUTCDate(), + date.getUTCMonth() + 1, + date.getUTCFullYear(), + ] + const dateString = date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + + const articleLink = `/news/${year}/${month}/${day}/${article.slug}`; + + return + + + + {dateString} + + { + article.newsOrBlog === 'blog' ? 'Blog' : 'Feature' + } + + + + {article.title} + + + + + .hover-link': { + position: 'absolute', + bottom: 0, + right: 0, + }, + position: 'relative', + maxHeight: 'var(--maxHeight)', + overflow: 'hidden', + }}> + {article.excerpt} + Read more → + + } \ No newline at end of file diff --git a/components/news/tag.js b/components/news/tag.js index 7159d1de..721f7695 100644 --- a/components/news/tag.js +++ b/components/news/tag.js @@ -25,7 +25,7 @@ const TYPES = { }, default: { bgColor: "#ededed", - color: "#414141", + color: "#474747", }, }; diff --git a/components/projectSpotlight.js b/components/projectSpotlight.js index 4a012ab5..6506e620 100644 --- a/components/projectSpotlight.js +++ b/components/projectSpotlight.js @@ -95,7 +95,7 @@ export const ProjectSpotlight = ({ selectedProjects }) => { return ( - Project Spotlight + Project Spotlight { + try { + let bodyContent = JSON.stringify({ + "pagination": { + "pageSize": 3, + "page": 1 + } + }); + + const response = await fetch(getStrapiURL('/api/post-list'), { + method: "POST", + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: bodyContent + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const result = await response.json(); + return result.results + + } catch (error) { + console.log(error); + } +} \ No newline at end of file diff --git a/lib/strapi/fetchOurWorkTrayItems.js b/lib/strapi/fetchOurWorkTrayItems.js index d0075a38..c953123f 100644 --- a/lib/strapi/fetchOurWorkTrayItems.js +++ b/lib/strapi/fetchOurWorkTrayItems.js @@ -33,7 +33,7 @@ export const fetchOurWorkTrayItems = async (preview = false) => { }`, preview ); - const payload = { + let payload = { collaborationCollection: data.collaborations.data.map((group) => ({ id: group.id, name: group.attributes.name, @@ -50,5 +50,11 @@ export const fetchOurWorkTrayItems = async (preview = false) => { slug: group.attributes.slug, })), }; + // sort everything alphabetically -- groups, collabs, and ops teams + payload = Object.keys(payload) + .reduce((acc, key) => { + acc[key] = payload[key].sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1) + return acc + }, {}) return payload; }; diff --git a/lib/strapi/fetchStrapiGraphQL.js b/lib/strapi/fetchStrapiGraphQL.js index 9d9f77e2..98796701 100644 --- a/lib/strapi/fetchStrapiGraphQL.js +++ b/lib/strapi/fetchStrapiGraphQL.js @@ -1,5 +1,7 @@ +import { getStrapiURL } from "@/utils/api"; + export const fetchStrapiGraphQL = async (query, preview = false, signal = undefined) => { - return fetch(`https://api.renci.org/graphql`, { + return fetch(getStrapiURL("/graphql"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/lib/strapi/index.js b/lib/strapi/index.js index 1806cd03..99d5120d 100644 --- a/lib/strapi/index.js +++ b/lib/strapi/index.js @@ -8,4 +8,5 @@ export * from './peopleGraphQL' export * from './fetchOurWorkTrayItems' export * from './newsAppearancesGraphQL' export * from './newsSWR' -export * from './newsGraphQL' \ No newline at end of file +export * from './newsGraphQL' +export * from './fetchHomeNews' \ No newline at end of file diff --git a/lib/strapi/newsGraphQL.js b/lib/strapi/newsGraphQL.js index 974d2dac..854b84de 100644 --- a/lib/strapi/newsGraphQL.js +++ b/lib/strapi/newsGraphQL.js @@ -1,5 +1,26 @@ import { fetchStrapiGraphQL } from "./fetchStrapiGraphQL"; +export const fetchPhotoThumbnailUrl = async (slug) => { + let res = await fetchStrapiGraphQL(` + query { + people(filters: { slug: { eq: "${slug}" }}) { + data { + attributes { + photo { + data { + attributes { + formats + } + } + } + } + } + } + } + `); + return res?.data?.people?.data?.[0]?.attributes?.photo?.data?.[0]?.attributes?.formats?.thumbnail ?? null; +} + export const fetchArticle = async (slug) => { const articleGql = await fetchStrapiGraphQL(` fragment PersonAttributes on PersonRelationResponseCollection { @@ -8,6 +29,7 @@ export const fetchArticle = async (slug) => { firstName lastName slug + active } } } @@ -105,10 +127,23 @@ export const fetchArticle = async (slug) => { if (articleGql?.data?.posts?.data?.length !== 1) return null; - return articleGql.data.posts.data.map(({ attributes }) => ({ + let photos = await Promise.allSettled(articleGql.data.posts.data[0].attributes.renciAuthors.data.map(({ attributes }) => ( + fetchPhotoThumbnailUrl(attributes.slug) + ))) + + console.log(photos); + + return await 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}`})), + renciAuthors: attributes.renciAuthors.data.map(({ attributes }, i) => ({ + ...attributes, + name: `${attributes.firstName} ${attributes.lastName}`, + photo: photos[i].status === "fulfilled" ? photos[i].value : null, + })), + 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), diff --git a/lib/strapi/newsSWR.js b/lib/strapi/newsSWR.js index 71f631d9..c9665bf8 100644 --- a/lib/strapi/newsSWR.js +++ b/lib/strapi/newsSWR.js @@ -1,3 +1,4 @@ +import { getStrapiURL } from '@/utils/api'; import useSWR from 'swr'; export const useTags = () => { @@ -16,7 +17,7 @@ export const useTags = () => { }); // dedupingInterval essentially sets cache expiration time to 1 hour - return useSWR('https://api.renci.org/api/all-post-tags', fetcher, { dedupingInterval: 1000 * 60 * 60 }); + return useSWR(getStrapiURL("/api/all-post-tags"), fetcher, { dedupingInterval: 1000 * 60 * 60 }); } /** @@ -72,7 +73,7 @@ export const useNewsArticles = ({ excerptLength, } - const articlesUrl = `https://api.renci.org/api/post-list`; + const articlesUrl = getStrapiURL("/api/post-list"); const cacheKey = JSON.stringify({articlesUrl, reqBody}); diff --git a/pages/[...slug].js b/pages/[...slug].js index f144467d..6977115c 100644 --- a/pages/[...slug].js +++ b/pages/[...slug].js @@ -5,6 +5,7 @@ import Seo from "@/components/elements/seo" import { useRouter } from "next/router" import Layout from "../components/layout" import { getLocalizedPaths } from "utils/localize" +import Head from "next/head" // The file is called [[...slug]].js because we're using Next's // optional catch all routes feature. See the related docs: @@ -14,6 +15,7 @@ const DynamicPage = ({ sections, metadata, preview, + title, global, pageContext }) => { @@ -31,6 +33,10 @@ const DynamicPage = ({ return (
+ {Boolean(title) && + { title } | RENCI.org + } + {/* Add meta tags for SEO*/} {/* Display content sections */} @@ -81,6 +87,7 @@ export async function getServerSideProps(context) { sections: contentSections, metadata: pageData.metaData, global: globalLocale, + title: pageData.title, pageContext: { ...pageContext, // localizedPaths, diff --git a/pages/index.js b/pages/index.js index 85adc18f..8fe7eb59 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,13 +1,16 @@ import { Fragment } from 'react' import Head from 'next/head' import Image from 'next/image' -import { Typography } from '@mui/material' +import { Typography, Stack } from '@mui/material' import { Link, Page } from '../components' import homeHero from '../images/racks.jpg' import { ProjectSpotlight } from '../components/projectSpotlight' import { fetchDashboardProjects } from "@/lib/dashboard/projects"; +import { fetchHomeNews } from '../lib/strapi' +import { HomePageArticlePreview } from "../components/news/article-preview"; + +export default function Home({ selectedProjects, newsArray }) { -export default function Home({ selectedProjects}) { return ( + { + newsArray && ( + + Recent News + + { newsArray.map((article, i) => ( + + ))} + + + ) + } + ) } export async function getServerSideProps({ res }) { - res.setHeader( - 'Cache-Control', - 'no-cache, no-store, must-revalidate' - ) + try { + res.setHeader( + 'Cache-Control', + 'no-cache, no-store, must-revalidate' + ) - const projects = await fetchDashboardProjects() - - let projectsCopy = [...projects] - let projectSelection = [] - for (let i = 0; i < 3; i += 1) { - const randomIndex = Math.floor(Math.random() * projectsCopy.length) - const randomProject = projectsCopy.splice(randomIndex, 1)[0] - //add a property that is a snippet of the original description before pushing to the array - projectSelection.push({ - ...randomProject, - }) - } + const [newsArray, projects] = await Promise.all([ + fetchHomeNews(), + fetchDashboardProjects(), + ]); + + let projectsCopy = [...projects] + let projectSelection = [] + for (let i = 0; i < 3; i += 1) { + const randomIndex = Math.floor(Math.random() * projectsCopy.length) + const randomProject = projectsCopy.splice(randomIndex, 1)[0] + //add a property that is a snippet of the original description before pushing to the array + projectSelection.push({ + ...randomProject, + }) + } - return { - props: { selectedProjects: JSON.parse(JSON.stringify(projectSelection)) }, + return { + props: { + selectedProjects: JSON.parse(JSON.stringify(projectSelection)), + newsArray: JSON.parse(JSON.stringify(newsArray)) + }, + } + } catch (error) { + console.error('Error fetching data:', error); + return { + props: { selectedProjects: [], newsArray: [] } + }; } } diff --git a/pages/news/[year]/[month]/[day]/[slug].js b/pages/news/[year]/[month]/[day]/[slug].js index daf7fe86..d1d8e67f 100644 --- a/pages/news/[year]/[month]/[day]/[slug].js +++ b/pages/news/[year]/[month]/[day]/[slug].js @@ -1,15 +1,17 @@ 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 { Divider, Typography, Stack, styled, Avatar } 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" +import { useRouter } from "next/router"; export default function Article({ article }) { + const router = useRouter(); const tags = [ article.projects.map((x) => ({ ...x, type: 'projects' })), @@ -24,6 +26,11 @@ export default function Article({ article }) { return `/news?${qs.stringify({[type]: id})}` } + let authors = [ + ...article.renciAuthors, + ...article.externalAuthors?.split(",").map((a) => a.trim()) ?? [] + ] + return ( @@ -60,17 +67,35 @@ export default function Article({ article }) { const id = type === 'postTags' ? name : slug; return ( - - - + { router.push(createTagLinkURL(id, type)); }} + sx={{ maxWidth: 'revert', cursor: 'pointer' }} + /> ) })} + {Boolean(authors.length) && + {authors.reduce((acc, a, i) => { + let out; + if (typeof a === "string") out = {a}; + else if (!a.active) out = {a.name}; + else out = + + {Boolean(a.photo) && } + {a.name} + + + + acc.push(out); + if(i < authors.length - 1) acc.push("·"); + return acc; + }, [])} + } + @@ -78,15 +103,18 @@ export default function Article({ article }) { { article.content.map((item)=> { return item.__typename == "ComponentPostSectionsImage" ? ( - {item.altText} +
+ {item.altText} + {item.caption} +
) : ( {item.content} ) @@ -95,64 +123,80 @@ export default function Article({ article }) { - - -
- {article.researchGroups[0] && ( - - Research Groups: -
    - { - article.researchGroups.map((item, i) => ( -
  • {item.name}
  • - )) - } -
-
-
- )} - {article.collaborations[0] && ( + { + ( article.researchGroups[0] || article.collaborations[0] || article.projects[0] || article.people[0] ) && ( - Collaborations: -
    - { - article.collaborations.map((item, i) => ( -
  • {item.name}
  • - )) - } -
-
+ + +
+ {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}
  • + )) + } +
+
+ )} +
- )} - {article.projects[0] && ( - - Projects: - { - article.projects.map((item, i) => ( -
  • {item.name}
  • - )) - } -
    -
    - )} - {article.people[0] && ( - - People: -
      - { - article.people.map((item, i) => ( -
    • {item.name}
    • - )) - } -
    -
    -
    - )} -
    + ) + }
    ) } +const Figure = styled('figure')` + margin: 1rem 0; + display: flex; + flex-direction: column; + gap: 8px; + + & figcaption.MuiTypography-root { + align-self: center; + font-style: italic; + } +`; + export async function getStaticPaths() { const postsGql = await fetchStrapiGraphQL(`query { posts(pagination: { limit: 1000 }, sort: "publishDate:desc") { diff --git a/pages/news/index.js b/pages/news/index.js index d8432413..a410d54d 100644 --- a/pages/news/index.js +++ b/pages/news/index.js @@ -111,7 +111,7 @@ export default function News() { const isTagSelected = useCallback((id, type) => { if (id === undefined || type === undefined) return false; return selectedTags[type]?.find((tag) => ( - tag[type === 'freeSearch' ? 'name' : 'slug'] === id + tag[type === 'freeSearch' || type === 'postTags' ? 'name' : 'slug'] === id )) !== undefined; }, [selectedTags]); diff --git a/pages/people/[slug].js b/pages/people/[slug].js index d80bcb20..fb885757 100644 --- a/pages/people/[slug].js +++ b/pages/people/[slug].js @@ -73,8 +73,8 @@ export default function Person({ person }) { { person.contributions.projects && ( - Projects -
      + Projects +
        { person.contributions.projects.map(project => (
      • @@ -86,12 +86,11 @@ export default function Person({ person }) { ) } - { person.contributions.projects && person.contributions.collaborations &&
        } { person.contributions.collaborations && ( - Collaborations -
          + Collaborations +
            { person.contributions.collaborations.map(project => (
          • diff --git a/style/theme.js b/style/theme.js index d6e84537..35533995 100644 --- a/style/theme.js +++ b/style/theme.js @@ -31,8 +31,9 @@ const typography = { fontSize: 'clamp(1.7rem, 1.486rem + 0.571vw, 2rem)', }, h3: { - fontSize: 'clamp(1.3rem, 0.986rem + 0.571vw, 1.7rem)', - paddingBottom: '0.5rem' + fontSize: '1.17rem', + paddingBottom: '0.5rem', + fontWeight: '500' }, h4: { fontSize: 'clamp(1.2rem, 3vw, 1.6rem)',