From 802285872a2c57e7a5e130f32a2b45497d7687f1 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 5 Dec 2023 19:11:34 +0100 Subject: refactor(pages): refine Projects page * add a `contexts` meta key to projects * replace `technologies` with `contexts` key in projects list * make getProjectsFilenames async * add Cypress tests --- src/content | 2 +- src/i18n/en.json | 12 ++- src/i18n/fr.json | 12 ++- src/pages/projets/[slug].tsx | 4 +- src/pages/projets/index.tsx | 149 +++++++++++++++----------- src/styles/abstracts/placeholders/_links.scss | 2 - src/styles/pages/projects.module.scss | 4 + src/types/data.ts | 3 +- src/utils/helpers/server/projects.ts | 55 ++++------ tests/cypress/e2e/pages/projects.cy.ts | 18 +++- 10 files changed, 153 insertions(+), 108 deletions(-) diff --git a/src/content b/src/content index fca1bc9..ff0f835 160000 --- a/src/content +++ b/src/content @@ -1 +1 @@ -Subproject commit fca1bc948fbe5ff2eca1194a1397aeb8a4f66057 +Subproject commit ff0f83536484454005340e9cd288f072639cc4e2 diff --git a/src/i18n/en.json b/src/i18n/en.json index 67880a2..64b3214 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -23,6 +23,10 @@ "defaultMessage": "It is now awaiting moderation.", "description": "PageComments: comment awaiting moderation" }, + "/STXAQ": { + "defaultMessage": "Read more about {title}", + "description": "ProjectsPage: card accessible name" + }, "/TTRRX": { "defaultMessage": "Breadcrumbs", "description": "Page: an accessible name for the breadcrumb nav." @@ -175,10 +179,6 @@ "defaultMessage": "Comment:", "description": "CommentForm: comment label" }, - "ADQmDF": { - "defaultMessage": "Technologies:", - "description": "Meta: technologies label" - }, "AN9iy7": { "defaultMessage": "Contact", "description": "ContactPage: page title" @@ -579,6 +579,10 @@ "defaultMessage": "Reading time:", "description": "PageHeader: reading time label" }, + "jPBeOI": { + "defaultMessage": "{contextsCount, plural, =0 {Contexts:} one {Context:} other {Contexts:}}", + "description": "ProjectsPage: context meta label" + }, "jrRBeb": { "defaultMessage": "Browse posts in {thematicName} thematic", "description": "ThematicPage: posts list heading" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 39ae75c..41af99d 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -23,6 +23,10 @@ "defaultMessage": "Il est maintenant en attente de modération.", "description": "PageComments: comment awaiting moderation" }, + "/STXAQ": { + "defaultMessage": "Consulter {title}", + "description": "ProjectsPage: card accessible name" + }, "/TTRRX": { "defaultMessage": "Fil d’Ariane", "description": "Page: an accessible name for the breadcrumb nav." @@ -175,10 +179,6 @@ "defaultMessage": "Commentaire :", "description": "CommentForm: comment label" }, - "ADQmDF": { - "defaultMessage": "Technologies :", - "description": "Meta: technologies label" - }, "AN9iy7": { "defaultMessage": "Contact", "description": "ContactPage: page title" @@ -579,6 +579,10 @@ "defaultMessage": "Temps de lecture :", "description": "PageHeader: reading time label" }, + "jPBeOI": { + "defaultMessage": "{contextsCount, plural, =0 {Contexte :} one {Contexte :} other {Contextes :}}", + "description": "ProjectsPage: context meta label" + }, "jrRBeb": { "defaultMessage": "Parcourir les articles de la thématique {thematicName}", "description": "ThematicPage: posts list heading" diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index ee88638..b4bc906 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -255,8 +255,8 @@ export const getStaticProps: GetStaticProps = async ({ }; }; -export const getStaticPaths: GetStaticPaths = () => { - const filenames = getProjectFilenames(); +export const getStaticPaths: GetStaticPaths = async () => { + const filenames = await getProjectFilenames(); const paths = filenames.map((filename) => { return { params: { diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx index 4e0bf92..843374a 100644 --- a/src/pages/projets/index.tsx +++ b/src/pages/projets/index.tsx @@ -1,7 +1,6 @@ import type { GetStaticProps } from 'next'; import Head from 'next/head'; import NextImage from 'next/image'; -import { useRouter } from 'next/router'; import Script from 'next/script'; import { useIntl } from 'react-intl'; import { @@ -13,11 +12,12 @@ import { CardTitle, getLayout, Grid, - MetaList, MetaItem, Page, PageHeader, PageBody, + CardMeta, + GridItem, } from '../../components'; import { mdxComponents } from '../../components/mdx'; import PageContent, { meta } from '../../content/pages/projects.mdx'; @@ -31,37 +31,33 @@ import { getWebPageSchema, } from '../../utils/helpers'; import { - getProjectsCard, + getAllProjects, loadTranslation, type Messages, } from '../../utils/helpers/server'; import { useBreadcrumb } from '../../utils/hooks'; type ProjectsPageProps = { - projects: ProjectPreview[]; + data: { + projects: ProjectPreview[]; + }; translation?: Messages; }; /** * Projects page. */ -const ProjectsPage: NextPageWithLayout = ({ projects }) => { +const ProjectsPage: NextPageWithLayout = ({ data }) => { const { dates, seo, title } = meta; const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, url: ROUTES.PROJECTS, }); const intl = useIntl(); - const metaLabel = intl.formatMessage({ - defaultMessage: 'Technologies:', - description: 'Meta: technologies label', - id: 'ADQmDF', - }); - const { asPath } = useRouter(); const webpageSchema = getWebPageSchema({ description: seo.description, locale: CONFIG.locales.defaultLocale, - slug: asPath, + slug: ROUTES.PROJECTS, title: seo.title, updateDate: dates.update, }); @@ -71,13 +67,13 @@ const ProjectsPage: NextPageWithLayout = ({ projects }) => { id: 'projects', kind: 'page', locale: CONFIG.locales.defaultLocale, - slug: asPath, + slug: ROUTES.PROJECTS, title, }); const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]); const page = { title: `${seo.title} - ${CONFIG.name}`, - url: `${CONFIG.url}${asPath}`, + url: `${CONFIG.url}${ROUTES.PROJECTS}`, }; return ( @@ -110,53 +106,84 @@ const ProjectsPage: NextPageWithLayout = ({ projects }) => { intro={} /> - - {projects.map( + + {data.projects.map( ({ id, meta: projectMeta, slug, title: projectTitle }) => { - const { cover, tagline, technologies } = projectMeta; - const figureLabel = intl.formatMessage( - { - defaultMessage: '{title} cover', - description: 'ProjectsPage: figure (cover) accessible name', - id: 'FdF33B', - }, - { title: projectTitle } - ); + const { contexts, cover, tagline } = projectMeta; + const messages = { + card: intl.formatMessage( + { + defaultMessage: 'Read more about {title}', + description: 'ProjectsPage: card accessible name', + id: '/STXAQ', + }, + { title: projectTitle } + ), + context: intl.formatMessage( + { + defaultMessage: + '{contextsCount, plural, =0 {Contexts:} one {Context:} other {Contexts:}}', + description: 'ProjectsPage: context meta label', + id: 'jPBeOI', + }, + { + contextsCount: contexts?.length, + } + ), + cover: intl.formatMessage( + { + defaultMessage: '{title} cover', + description: 'ProjectsPage: figure (cover) accessible name', + id: 'FdF33B', + }, + { title: projectTitle } + ), + }; return ( - - - - ) : undefined - } - key={id} - meta={ - technologies ? ( - - { - return { id: techno, value: techno }; - })} - /> - - ) : undefined - } - isCentered - linkTo={`${ROUTES.PROJECTS}/${slug}`} - > - - {projectTitle} - - {tagline} - - + + + + + ) : undefined + } + meta={ + contexts?.length ? ( + + { + return { id: context, value: context }; + })} + /> + + ) : undefined + } + isCentered + linkTo={slug} + > + + {projectTitle} + + {tagline} + + + ); } )} @@ -171,12 +198,14 @@ ProjectsPage.getLayout = (page) => getLayout(page); export const getStaticProps: GetStaticProps = async ({ locale, }) => { - const projects = await getProjectsCard(); + const projects = await getAllProjects(); const translation = await loadTranslation(locale); return { props: { - projects: JSON.parse(JSON.stringify(projects)), + data: { + projects: JSON.parse(JSON.stringify(projects)), + }, translation, }, }; diff --git a/src/styles/abstracts/placeholders/_links.scss b/src/styles/abstracts/placeholders/_links.scss index 9bfd19e..a230e70 100644 --- a/src/styles/abstracts/placeholders/_links.scss +++ b/src/styles/abstracts/placeholders/_links.scss @@ -29,8 +29,6 @@ } %link-with-icon { - display: inline-block; - &::after { display: inline-block; content: var(--is-lang-hidden, "\0000a0" var(--lang-icon, "")) diff --git a/src/styles/pages/projects.module.scss b/src/styles/pages/projects.module.scss index f72c573..33a0d42 100644 --- a/src/styles/pages/projects.module.scss +++ b/src/styles/pages/projects.module.scss @@ -9,3 +9,7 @@ } } } + +.card { + height: 100%; +} diff --git a/src/types/data.ts b/src/types/data.ts index 80a8bf3..1d0746d 100644 --- a/src/types/data.ts +++ b/src/types/data.ts @@ -252,6 +252,7 @@ export type Repos = { }; export type ProjectMeta = Omit & { + contexts?: string[]; license?: string; repos?: Repos; tagline?: string; @@ -264,7 +265,7 @@ export type Project = Omit & { }; export type ProjectPreview = Omit & { - meta: Omit; + meta: Pick; }; export type ThematicMeta = Omit & { diff --git a/src/utils/helpers/server/projects.ts b/src/utils/helpers/server/projects.ts index c1a3d10..3f822fe 100644 --- a/src/utils/helpers/server/projects.ts +++ b/src/utils/helpers/server/projects.ts @@ -1,15 +1,17 @@ -import { readdirSync } from 'fs'; -import path from 'path'; +import { readdir } from 'fs/promises'; +import { join } from 'path'; +import type { StaticImageData } from 'next/image'; import type { MDXProjectMeta, Project, ProjectPreview } from '../../../types'; +import { ROUTES } from '../../constants'; /** * Retrieve all the projects filename. * - * @returns {string[]} An array of filenames. + * @returns {Promise} An array of filenames. */ -export const getProjectFilenames = (): string[] => { - const projectsDirectory = path.join(process.cwd(), 'src/content/projects'); - const filenames = readdirSync(projectsDirectory); +export const getProjectFilenames = async (): Promise => { + const projectsDir = join(process.cwd(), 'src/content/projects'); + const filenames = await readdir(projectsDir); return filenames.map((filename) => filename.replace(/\.mdx$/, '')); }; @@ -18,7 +20,7 @@ export const getProjectFilenames = (): string[] => { * Retrieve the data of a project by filename. * * @param {string} filename - The project filename. - * @returns {Promise} + * @returns {Promise} A ProjectPreview object. */ export const getProjectData = async (filename: string): Promise => { try { @@ -28,25 +30,26 @@ export const getProjectData = async (filename: string): Promise => { meta: MDXProjectMeta; } = await import(`../../../content/projects/${filename}.mdx`); - const { dates, intro, title, ...projectMeta } = meta; - const { publication, update } = dates; - const cover = await import(`../../../../public/projects/${filename}.jpg`); + const { intro, title, ...projectMeta } = meta; + const cover: StaticImageData = ( + await import(`../../../../public/projects/${filename}.jpg`) + ).default; return { id: filename, intro, meta: { ...projectMeta, - dates: { publication, update }, // Dynamic import source does not work so I use it only to get sizes cover: { - ...cover.default, alt: `${title} image`, + height: cover.height, src: `/projects/${filename}.jpg`, + width: cover.width, title, }, }, - slug: filename, + slug: `${ROUTES.PROJECTS}/${filename}`, title, }; } catch (err) { @@ -55,28 +58,14 @@ export const getProjectData = async (filename: string): Promise => { } }; -/** - * Retrieve all the projects data using filenames. - * - * @param {string[]} filenames - The filenames without extension. - * @returns {Promise} - An array of projects data. - */ -export const getProjectsData = async ( - filenames: string[] -): Promise => - Promise.all(filenames.map(async (filename) => getProjectData(filename))); - /** * Method to sort an array of projects by publication date. * * @param {ProjectPreview} a - A single project. * @param {ProjectPreview} b - A single project. - * @returns The result used by Array.sort() method: 1 || -1 || 0. + * @returns {number} The result used by Array.sort() method: 1 || -1 || 0. */ -const sortProjectsByPublicationDate = ( - a: ProjectPreview, - b: ProjectPreview -) => { +const byPublicationDate = (a: ProjectPreview, b: ProjectPreview) => { if (a.meta.dates.publication < b.meta.dates.publication) return 1; if (a.meta.dates.publication > b.meta.dates.publication) return -1; return 0; @@ -87,9 +76,9 @@ const sortProjectsByPublicationDate = ( * * @returns {Promise} An array of projects. */ -export const getProjectsCard = async (): Promise => { - const filenames = getProjectFilenames(); - const projects = await getProjectsData(filenames); +export const getAllProjects = async (): Promise => { + const filenames = await getProjectFilenames(); + const projects = await Promise.all(filenames.map(getProjectData)); - return [...projects].sort(sortProjectsByPublicationDate); + return [...projects].sort(byPublicationDate); }; diff --git a/tests/cypress/e2e/pages/projects.cy.ts b/tests/cypress/e2e/pages/projects.cy.ts index b477400..5931ffe 100644 --- a/tests/cypress/e2e/pages/projects.cy.ts +++ b/tests/cypress/e2e/pages/projects.cy.ts @@ -1,6 +1,22 @@ +import { ROUTES } from '../../../../src/utils/constants'; + describe('Projects Page', () => { + beforeEach(() => { + cy.visit(ROUTES.PROJECTS); + }); + it('successfully loads', () => { - cy.visit('/projets'); cy.findByRole('heading', { level: 1 }).contains('Projets'); }); + + it('contains a breadcrumbs', () => { + cy.findByRole('navigation', { name: 'Fil d’Ariane' }).should('exist'); + }); + + it('can list the projects', () => { + cy.findAllByRole('link', { name: /Consulter/ }).should( + 'have.length.at.least', + 1 + ); + }); }); -- cgit v1.2.3