diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-12-05 19:11:34 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-12-07 19:12:11 +0100 |
| commit | 802285872a2c57e7a5e130f32a2b45497d7687f1 (patch) | |
| tree | 9803af7e06f8b59353e5458f33e54d02b4b9613b | |
| parent | e9d5a40432c451090e17859c764e52a96120b712 (diff) | |
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
| m--------- | src/content | 0 | ||||
| -rw-r--r-- | src/i18n/en.json | 12 | ||||
| -rw-r--r-- | src/i18n/fr.json | 12 | ||||
| -rw-r--r-- | src/pages/projets/[slug].tsx | 4 | ||||
| -rw-r--r-- | src/pages/projets/index.tsx | 149 | ||||
| -rw-r--r-- | src/styles/abstracts/placeholders/_links.scss | 2 | ||||
| -rw-r--r-- | src/styles/pages/projects.module.scss | 4 | ||||
| -rw-r--r-- | src/types/data.ts | 3 | ||||
| -rw-r--r-- | src/utils/helpers/server/projects.ts | 55 | ||||
| -rw-r--r-- | tests/cypress/e2e/pages/projects.cy.ts | 18 |
10 files changed, 152 insertions, 107 deletions
diff --git a/src/content b/src/content -Subproject fca1bc948fbe5ff2eca1194a1397aeb8a4f6605 +Subproject ff0f83536484454005340e9cd288f072639cc4e 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<ProjectPageProps> = 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<ProjectsPageProps> = ({ projects }) => { +const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ 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<ProjectsPageProps> = ({ 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<ProjectsPageProps> = ({ projects }) => { intro={<PageContent components={mdxComponents} />} /> <PageBody className={styles.body}> - <Grid className={styles.list} gap="sm" isCentered sizeMax="30ch"> - {projects.map( + <Grid + className={styles.list} + // eslint-disable-next-line react/jsx-no-literals + gap="sm" + isCentered + // eslint-disable-next-line react/jsx-no-literals + sizeMax="30ch" + > + {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 ( - <Card - cover={ - cover ? ( - <CardCover aria-label={figureLabel} hasBorders> - <NextImage {...cover} /> - </CardCover> - ) : undefined - } - key={id} - meta={ - technologies ? ( - <MetaList isCentered> - <MetaItem - hasBorderedValues - hasInlinedValues - isCentered - label={metaLabel} - value={technologies.map((techno) => { - return { id: techno, value: techno }; - })} - /> - </MetaList> - ) : undefined - } - isCentered - linkTo={`${ROUTES.PROJECTS}/${slug}`} - > - <CardHeader> - <CardTitle>{projectTitle}</CardTitle> - </CardHeader> - <CardBody>{tagline}</CardBody> - <CardFooter /> - </Card> + <GridItem key={id}> + <Card + aria-label={messages.card} + className={styles.card} + cover={ + cover ? ( + <CardCover aria-label={messages.cover} hasBorders> + <NextImage {...cover} /> + </CardCover> + ) : undefined + } + meta={ + contexts?.length ? ( + <CardMeta isCentered> + <MetaItem + hasBorderedValues + hasInlinedValues + isCentered + label={messages.context} + value={contexts.map((context) => { + return { id: context, value: context }; + })} + /> + </CardMeta> + ) : undefined + } + isCentered + linkTo={slug} + > + <CardHeader> + <CardTitle>{projectTitle}</CardTitle> + </CardHeader> + <CardBody>{tagline}</CardBody> + <CardFooter /> + </Card> + </GridItem> ); } )} @@ -171,12 +198,14 @@ ProjectsPage.getLayout = (page) => getLayout(page); export const getStaticProps: GetStaticProps<ProjectsPageProps> = 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<PageMeta, 'wordsCount'> & { + contexts?: string[]; license?: string; repos?: Repos; tagline?: string; @@ -264,7 +265,7 @@ export type Project = Omit<Page, 'content'> & { }; export type ProjectPreview = Omit<Project, 'meta'> & { - meta: Omit<ProjectMeta, 'license' | 'repos'>; + meta: Pick<ProjectMeta, 'contexts' | 'cover' | 'dates' | 'tagline'>; }; export type ThematicMeta = Omit<PageMeta, 'wordsCount'> & { 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<string[]>} 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<string[]> => { + 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<ProjectPreview>} + * @returns {Promise<ProjectPreview>} A ProjectPreview object. */ export const getProjectData = async (filename: string): Promise<Project> => { try { @@ -28,25 +30,26 @@ export const getProjectData = async (filename: string): Promise<Project> => { 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) { @@ -56,27 +59,13 @@ export const getProjectData = async (filename: string): Promise<Project> => { }; /** - * Retrieve all the projects data using filenames. - * - * @param {string[]} filenames - The filenames without extension. - * @returns {Promise<ProjectPreview[]>} - An array of projects data. - */ -export const getProjectsData = async ( - filenames: string[] -): Promise<ProjectPreview[]> => - 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<ProjectPreview[]>} An array of projects. */ -export const getProjectsCard = async (): Promise<ProjectPreview[]> => { - const filenames = getProjectFilenames(); - const projects = await getProjectsData(filenames); +export const getAllProjects = async (): Promise<ProjectPreview[]> => { + 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 + ); + }); }); |
