aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
m---------src/content0
-rw-r--r--src/i18n/en.json12
-rw-r--r--src/i18n/fr.json12
-rw-r--r--src/pages/projets/[slug].tsx4
-rw-r--r--src/pages/projets/index.tsx149
-rw-r--r--src/styles/abstracts/placeholders/_links.scss2
-rw-r--r--src/styles/pages/projects.module.scss4
-rw-r--r--src/types/data.ts3
-rw-r--r--src/utils/helpers/server/projects.ts55
-rw-r--r--tests/cypress/e2e/pages/projects.cy.ts18
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
+ );
+ });
});