aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--jest.config.js4
-rw-r--r--src/components/organisms/index.ts1
-rw-r--r--src/components/organisms/layout/index.ts1
-rw-r--r--src/components/organisms/layout/overview.module.scss37
-rw-r--r--src/components/organisms/layout/overview.stories.tsx78
-rw-r--r--src/components/organisms/layout/overview.test.tsx33
-rw-r--r--src/components/organisms/layout/overview.tsx44
-rw-r--r--src/components/organisms/project-overview/index.ts1
-rw-r--r--src/components/organisms/project-overview/project-overview.module.scss40
-rw-r--r--src/components/organisms/project-overview/project-overview.stories.tsx78
-rw-r--r--src/components/organisms/project-overview/project-overview.test.tsx127
-rw-r--r--src/components/organisms/project-overview/project-overview.tsx193
-rw-r--r--src/i18n/en.json56
-rw-r--r--src/i18n/fr.json60
-rw-r--r--src/pages/projets/[slug].tsx162
-rw-r--r--src/types/generics.ts7
-rw-r--r--tests/jest/__mocks__/svgr.mock.tsx9
17 files changed, 563 insertions, 368 deletions
diff --git a/jest.config.js b/jest.config.js
index 77cabb1..34f960e 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -18,6 +18,10 @@ const customJestConfig = {
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
moduleDirectories: ['node_modules', '<rootDir>/'],
+ moduleNameMapper: {
+ '^.+\\.(svg)$': '<rootDir>/tests/jest/__mocks__/svgr.mock.tsx',
+ },
+
// Add more setup options before each test is run
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts
index 4e62dd1..04a985d 100644
--- a/src/components/organisms/index.ts
+++ b/src/components/organisms/index.ts
@@ -5,4 +5,5 @@ export * from './layout';
export * from './nav';
export * from './navbar';
export * from './post-preview';
+export * from './project-overview';
export * from './widgets';
diff --git a/src/components/organisms/layout/index.ts b/src/components/organisms/layout/index.ts
index ebe48e7..03fba32 100644
--- a/src/components/organisms/layout/index.ts
+++ b/src/components/organisms/layout/index.ts
@@ -1,3 +1,2 @@
export * from './no-results';
-export * from './overview';
export * from './posts-list';
diff --git a/src/components/organisms/layout/overview.module.scss b/src/components/organisms/layout/overview.module.scss
deleted file mode 100644
index c1d9463..0000000
--- a/src/components/organisms/layout/overview.module.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-@use "../../../styles/abstracts/mixins" as mix;
-
-.wrapper {
- padding: var(--spacing-sm) var(--spacing-md);
- border: fun.convert-px(1) solid var(--color-border);
-
- .meta {
- display: grid;
- grid-template-columns: repeat(
- auto-fit,
- min(calc(100vw - (var(--spacing-md) * 2)), 23ch)
- );
- row-gap: var(--spacing-sm);
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- grid-template-columns: repeat(
- auto-fit,
- min(calc(100vw - (var(--spacing-md) * 2)), 20ch)
- );
- }
- }
- }
-
- .cover {
- width: fit-content;
- height: fun.convert-px(175);
- margin-bottom: var(--spacing-sm);
- padding: var(--spacing-2xs);
- border: fun.convert-px(1) solid var(--color-border);
-
- img {
- object-fit: contain;
- }
- }
-}
diff --git a/src/components/organisms/layout/overview.stories.tsx b/src/components/organisms/layout/overview.stories.tsx
deleted file mode 100644
index 562d7c4..0000000
--- a/src/components/organisms/layout/overview.stories.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import type { MetaItemData } from '../../molecules';
-import { Overview } from './overview';
-
-/**
- * Overview - Storybook Meta
- */
-export default {
- title: 'Organisms/Layout/Overview',
- component: Overview,
- argTypes: {
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the overview wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- cover: {
- description: 'The overview cover',
- table: {
- category: 'Options',
- },
- type: {
- name: 'object',
- required: false,
- value: {},
- },
- },
- meta: {
- description: 'The overview meta.',
- type: {
- name: 'object',
- required: true,
- value: {},
- },
- },
- },
-} as ComponentMeta<typeof Overview>;
-
-const Template: ComponentStory<typeof Overview> = (args) => (
- <Overview {...args} />
-);
-
-const cover = {
- alt: 'picture',
- height: 480,
- src: 'https://picsum.photos/640/480',
- width: 640,
-};
-
-const meta = [
- { id: 'creation-date', label: 'Creation date', value: '2022-05-09' },
- { id: 'license', label: 'License', value: 'Dignissimos ratione veritatis' },
-] satisfies MetaItemData[];
-
-/**
- * Overview Stories - Default
- */
-export const Default = Template.bind({});
-Default.args = {
- meta,
-};
-
-/**
- * Overview Stories - With cover
- */
-export const WithCover = Template.bind({});
-WithCover.args = {
- cover,
- meta,
-};
diff --git a/src/components/organisms/layout/overview.test.tsx b/src/components/organisms/layout/overview.test.tsx
deleted file mode 100644
index b98bd6f..0000000
--- a/src/components/organisms/layout/overview.test.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen as rtlScreen } from '../../../../tests/utils';
-import type { MetaItemData } from '../../molecules';
-import { Overview } from './overview';
-
-const cover = {
- alt: 'Incidunt unde quam',
- height: 480,
- src: 'https://picsum.photos/640/480',
- width: 640,
-};
-
-const meta = [
- { id: 'creation-date', label: 'Creation date', value: '2022-05-09' },
- { id: 'license', label: 'License', value: 'Dignissimos ratione veritatis' },
-] satisfies MetaItemData[];
-
-describe('Overview', () => {
- it('renders some meta', () => {
- render(<Overview meta={meta} />);
-
- const metaLabels = meta.map((item) => item.label);
-
- for (const label of metaLabels) {
- expect(rtlScreen.getByText(label)).toBeInTheDocument();
- }
- });
-
- it('renders a cover', () => {
- render(<Overview cover={cover} meta={meta} />);
- expect(rtlScreen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
- });
-});
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
deleted file mode 100644
index ede2627..0000000
--- a/src/components/organisms/layout/overview.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import NextImage, { type ImageProps as NextImageProps } from 'next/image';
-import type { FC } from 'react';
-import { Figure } from '../../atoms';
-import { MetaList, type MetaItemData } from '../../molecules';
-import styles from './overview.module.scss';
-
-export type OverviewProps = {
- /**
- * Set additional classnames to the overview wrapper.
- */
- className?: string;
- /**
- * The overview cover.
- */
- cover?: Pick<NextImageProps, 'alt' | 'src' | 'width' | 'height'>;
- /**
- * The overview meta.
- */
- meta: MetaItemData[];
-};
-
-/**
- * Overview component
- *
- * Render an overview.
- */
-export const Overview: FC<OverviewProps> = ({
- className = '',
- cover,
- meta,
-}) => {
- const wrapperClass = `${styles.wrapper} ${className}`;
-
- return (
- <div className={wrapperClass}>
- {cover ? (
- <Figure>
- <NextImage {...cover} className={styles.cover} />
- </Figure>
- ) : null}
- <MetaList className={styles.meta} hasInlinedValues items={meta} />
- </div>
- );
-};
diff --git a/src/components/organisms/project-overview/index.ts b/src/components/organisms/project-overview/index.ts
new file mode 100644
index 0000000..41f3e8c
--- /dev/null
+++ b/src/components/organisms/project-overview/index.ts
@@ -0,0 +1 @@
+export * from './project-overview';
diff --git a/src/components/organisms/project-overview/project-overview.module.scss b/src/components/organisms/project-overview/project-overview.module.scss
new file mode 100644
index 0000000..c9e4edb
--- /dev/null
+++ b/src/components/organisms/project-overview/project-overview.module.scss
@@ -0,0 +1,40 @@
+@use "../../../styles/abstracts/functions" as fun;
+@use "../../../styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: flex;
+ flex-flow: column wrap;
+ align-items: center;
+ width: fit-content;
+ row-gap: var(--spacing-md);
+ margin-inline: auto;
+ padding: var(--spacing-md);
+ border: fun.convert-px(1) solid var(--color-border);
+}
+
+.heading {
+ margin-bottom: var(--spacing-md);
+}
+
+.cover {
+ height: fit-content;
+ margin: 0;
+
+ > img {
+ width: 100%;
+ max-height: fun.convert-px(250);
+ object-fit: cover;
+ }
+}
+
+.meta {
+ grid-auto-flow: row;
+ column-gap: clamp(var(--spacing-sm), 1.5vw, var(--spacing-lg));
+ row-gap: clamp(var(--spacing-2xs), 1vw, var(--spacing-sm));
+
+ @include mix.media("screen") {
+ @include mix.dimensions("xs") {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+ }
+}
diff --git a/src/components/organisms/project-overview/project-overview.stories.tsx b/src/components/organisms/project-overview/project-overview.stories.tsx
new file mode 100644
index 0000000..655dc3c
--- /dev/null
+++ b/src/components/organisms/project-overview/project-overview.stories.tsx
@@ -0,0 +1,78 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import NextImage from 'next/image';
+import { type ProjectMeta, ProjectOverview } from './project-overview';
+
+/**
+ * ProjectOverview - Storybook Meta
+ */
+export default {
+ title: 'Organisms/ProjectOverview',
+ component: ProjectOverview,
+ argTypes: {
+ cover: {
+ description: 'The project cover',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ meta: {
+ description: 'The overview meta.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'The project name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof ProjectOverview>;
+
+const Template: ComponentStory<typeof ProjectOverview> = (args) => (
+ <ProjectOverview {...args} />
+);
+
+const meta = {
+ creationDate: '2015-09-02',
+ lastUpdateDate: '2023-11-10',
+ license: 'MIT',
+} satisfies Partial<ProjectMeta>;
+
+/**
+ * ProjectOverview Stories - Meta
+ */
+export const Meta = Template.bind({});
+Meta.args = {
+ meta,
+ name: 'Your project',
+};
+
+/**
+ * ProjectOverview Stories - With cover
+ */
+export const WithCover = Template.bind({});
+WithCover.args = {
+ cover: (
+ <NextImage
+ alt=""
+ height={480}
+ src="https://picsum.photos/640/480"
+ width={640}
+ />
+ ),
+ meta,
+ name: 'Your project',
+};
diff --git a/src/components/organisms/project-overview/project-overview.test.tsx b/src/components/organisms/project-overview/project-overview.test.tsx
new file mode 100644
index 0000000..6234368
--- /dev/null
+++ b/src/components/organisms/project-overview/project-overview.test.tsx
@@ -0,0 +1,127 @@
+import { describe, expect, it } from '@jest/globals';
+import NextImage from 'next/image';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
+import { type ProjectMeta, ProjectOverview } from './project-overview';
+
+describe('ProjectOverview', () => {
+ it('can render a meta for the creation date', () => {
+ const meta = {
+ creationDate: '2023-11-01',
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Created on:');
+ });
+
+ it('can render a meta for the update date', () => {
+ const meta = {
+ lastUpdateDate: '2023-11-02',
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Updated on:');
+ });
+
+ it('can render a meta for the license', () => {
+ const meta = {
+ license: 'MIT',
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('License:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(meta.license);
+ });
+
+ it('can render a meta for the popularity', () => {
+ const meta = {
+ popularity: { count: 5 },
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Popularity:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(
+ `${meta.popularity.count} stars`
+ );
+ });
+
+ it('can render a meta for the popularity with a link', () => {
+ const meta = {
+ popularity: { count: 3, url: '#popularity' },
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Popularity:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(
+ `${meta.popularity.count} stars`
+ );
+ expect(rtlScreen.getByRole('link')).toHaveAttribute(
+ 'href',
+ meta.popularity.url
+ );
+ });
+
+ it('can render a meta for the technologies', () => {
+ const meta = {
+ technologies: ['Javascript', 'React'],
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Technologies:');
+ expect(rtlScreen.getAllByRole('definition')).toHaveLength(
+ meta.technologies.length
+ );
+ });
+
+ it('can render a meta for the repositories', () => {
+ const meta = {
+ repositories: [{ id: 'Github', label: 'Github', url: '#github' }],
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Repositories:');
+ expect(rtlScreen.getAllByRole('definition')).toHaveLength(
+ meta.repositories.length
+ );
+ expect(
+ rtlScreen.getByRole('link', { name: meta.repositories[0].label })
+ ).toHaveAttribute('href', meta.repositories[0].url);
+ });
+
+ it('can render a cover', () => {
+ const altTxt = 'id qui nisi';
+
+ render(
+ <ProjectOverview
+ cover={
+ <NextImage
+ alt={altTxt}
+ height={480}
+ src="https://picsum.photos/640/480"
+ width={640}
+ />
+ }
+ meta={{}}
+ name="qui"
+ />
+ );
+
+ expect(rtlScreen.getByRole('img')).toHaveAccessibleName(altTxt);
+ });
+
+ it('does not render a meta if the key is undefined', () => {
+ const meta = {
+ creationDate: undefined,
+ } satisfies Partial<ProjectMeta>;
+
+ render(<ProjectOverview meta={meta} name="quo" />);
+
+ expect(rtlScreen.queryByRole('term')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/project-overview/project-overview.tsx b/src/components/organisms/project-overview/project-overview.tsx
new file mode 100644
index 0000000..2b8be0e
--- /dev/null
+++ b/src/components/organisms/project-overview/project-overview.tsx
@@ -0,0 +1,193 @@
+import {
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ forwardRef,
+ useCallback,
+ type ReactElement,
+} from 'react';
+import { useIntl } from 'react-intl';
+import type { Maybe, ValueOf } from '../../../types';
+import {
+ Time,
+ type SocialWebsite,
+ Link,
+ SocialLink,
+ Figure,
+} from '../../atoms';
+import { MetaList, type MetaItemData } from '../../molecules';
+import styles from './project-overview.module.scss';
+
+export type Repository = {
+ id: Extract<SocialWebsite, 'Github' | 'Gitlab'>;
+ label: string;
+ url: string;
+};
+
+export type ProjectPopularity = {
+ count: number;
+ url?: string;
+};
+
+export type ProjectMeta = {
+ creationDate: string;
+ lastUpdateDate: string;
+ license: string;
+ popularity: ProjectPopularity;
+ repositories: Repository[];
+ technologies: string[];
+};
+
+const validMeta = [
+ 'creationDate',
+ 'lastUpdateDate',
+ 'license',
+ 'popularity',
+ 'repositories',
+ 'technologies',
+] satisfies (keyof ProjectMeta)[];
+
+const isValidMetaKey = (key: string): key is keyof ProjectMeta =>
+ (validMeta as string[]).includes(key);
+
+export type ProjectOverviewProps = Omit<
+ HTMLAttributes<HTMLDivElement>,
+ 'children'
+> & {
+ /**
+ * The project cover.
+ */
+ cover?: ReactElement;
+ /**
+ * The project meta.
+ */
+ meta: Partial<ProjectMeta>;
+ /**
+ * The project name.
+ */
+ name: string;
+};
+
+const ProjectOverviewWithRef: ForwardRefRenderFunction<
+ HTMLDivElement,
+ ProjectOverviewProps
+> = ({ className = '', cover, meta, name, ...props }, ref) => {
+ const wrapperClass = `${styles.wrapper} ${className}`;
+ const intl = useIntl();
+ const coverLabel = intl.formatMessage(
+ {
+ defaultMessage: 'Illustration of {projectName}',
+ description: 'ProjectOverview: cover accessible name',
+ id: '701ggm',
+ },
+ { projectName: name }
+ );
+ const metaLabels = {
+ creationDate: intl.formatMessage({
+ defaultMessage: 'Created on:',
+ description: 'ProjectOverview: creation date label',
+ id: 'c0Oecl',
+ }),
+ lastUpdateDate: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'ProjectOverview: update date label',
+ id: 'JbT+fA',
+ }),
+ license: intl.formatMessage({
+ defaultMessage: 'License:',
+ description: 'ProjectOverview: license label',
+ id: 'QtdnFV',
+ }),
+ popularity: intl.formatMessage({
+ defaultMessage: 'Popularity:',
+ description: 'ProjectOverview: popularity label',
+ id: 'cIAOyy',
+ }),
+ repositories: intl.formatMessage({
+ defaultMessage: 'Repositories:',
+ description: 'ProjectOverview: repositories label',
+ id: '3bKzk0',
+ }),
+ technologies: intl.formatMessage({
+ defaultMessage: 'Technologies:',
+ description: 'ProjectOverview: technologies label',
+ id: 'OWkqXt',
+ }),
+ } satisfies Record<keyof ProjectMeta, string>;
+
+ const getMetaValue = useCallback(
+ (key: keyof ProjectMeta, value: ValueOf<ProjectMeta>) => {
+ if (typeof value === 'string') {
+ return key === 'license' ? value : <Time date={value} />;
+ }
+
+ if (
+ (value instanceof Object || typeof value === 'object') &&
+ !Array.isArray(value)
+ ) {
+ const stars = intl.formatMessage(
+ {
+ defaultMessage:
+ '{starsCount, plural, =0 {No stars} one {# star} other {# stars}}',
+ description: 'ProjectOverview: stars count',
+ id: 'PBdVsm',
+ },
+ { starsCount: value.count }
+ );
+
+ return value.url ? (
+ <>
+ ⭐&nbsp;<Link href={value.url}>{stars}</Link>
+ </>
+ ) : (
+ `⭐\u00A0${stars}`
+ );
+ }
+
+ return value.map((v) => {
+ if (typeof v === 'string') return { id: v, value: v };
+
+ return {
+ id: v.id,
+ value: <SocialLink icon={v.id} label={v.label} url={v.url} />,
+ };
+ });
+ },
+ [intl]
+ );
+
+ const getMetaItems = useCallback((): MetaItemData[] => {
+ const keys = Object.keys(meta).filter(isValidMetaKey);
+
+ return keys
+ .map((key): Maybe<MetaItemData> => {
+ const value = meta[key];
+
+ return value
+ ? {
+ id: key,
+ label: metaLabels[key],
+ value: getMetaValue(key, value),
+ hasBorderedValues: key === 'technologies',
+ hasInlinedValues:
+ (key === 'technologies' || key === 'repositories') &&
+ Array.isArray(value) &&
+ value.length > 1,
+ }
+ : undefined;
+ })
+ .filter((item): item is MetaItemData => typeof item !== 'undefined');
+ }, [getMetaValue, meta, metaLabels]);
+
+ return (
+ <div {...props} className={wrapperClass} ref={ref}>
+ {cover ? (
+ <Figure aria-label={coverLabel} className={styles.cover} hasBorders>
+ {cover}
+ </Figure>
+ ) : null}
+ <MetaList className={styles.meta} isInline items={getMetaItems()} />
+ </div>
+ );
+};
+
+export const ProjectOverview = forwardRef(ProjectOverviewWithRef);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6d7af16..094bf56 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -87,6 +87,10 @@
"defaultMessage": "Thanks. Your message was successfully sent. I will answer it as soon as possible.",
"description": "Contact: success message"
},
+ "3bKzk0": {
+ "defaultMessage": "Repositories:",
+ "description": "ProjectOverview: repositories label"
+ },
"3f3PzH": {
"defaultMessage": "Github",
"description": "HomePage: Github link"
@@ -123,6 +127,10 @@
"defaultMessage": "Copy",
"description": "usePrism: copy button text (not clicked)"
},
+ "701ggm": {
+ "defaultMessage": "Illustration of {projectName}",
+ "description": "ProjectOverview: cover accessible name"
+ },
"75FYp7": {
"defaultMessage": "Github profile",
"description": "ContactPage: Github profile link"
@@ -287,6 +295,10 @@
"defaultMessage": "Submitting...",
"description": "CommentForm: spinner message on submit"
},
+ "JbT+fA": {
+ "defaultMessage": "Updated on:",
+ "description": "ProjectOverview: update date label"
+ },
"Jm0a6H": {
"defaultMessage": "Github profile",
"description": "CVPage: Github profile link"
@@ -315,10 +327,6 @@
"defaultMessage": "Page not found",
"description": "Error404Page: page title"
},
- "KrNvQi": {
- "defaultMessage": "Popularity:",
- "description": "ProjectsPage: popularity label"
- },
"LszkU6": {
"defaultMessage": "All posts in {thematicName}",
"description": "ThematicPage: posts list heading"
@@ -359,10 +367,18 @@
"defaultMessage": "Publish",
"description": "CommentForm: submit button"
},
+ "OWkqXt": {
+ "defaultMessage": "Technologies:",
+ "description": "ProjectOverview: technologies label"
+ },
"OevMeU": {
"defaultMessage": "{minutesCount} minutes {secondsCount} seconds",
"description": "useReadingTime: minutes + seconds count"
},
+ "PBdVsm": {
+ "defaultMessage": "{starsCount, plural, =0 {No stars} one {# star} other {# stars}}",
+ "description": "ProjectOverview: stars count"
+ },
"PHO94k": {
"defaultMessage": "Go to previous page",
"description": "PostsList: pagination backward link label"
@@ -391,6 +407,10 @@
"defaultMessage": "Find me elsewhere",
"description": "ContactPage: social media widget title"
},
+ "QtdnFV": {
+ "defaultMessage": "License:",
+ "description": "ProjectOverview: license label"
+ },
"R895yC": {
"defaultMessage": "CV",
"description": "Layout: main nav - cv link"
@@ -407,10 +427,6 @@
"defaultMessage": "Loading the repository popularity...",
"description": "ProjectsPage: loading repository popularity"
},
- "RwNZ6p": {
- "defaultMessage": "Technologies:",
- "description": "ProjectsPage: technologies label"
- },
"Sm2wCk": {
"defaultMessage": "LinkedIn profile",
"description": "CVPage: LinkedIn profile link"
@@ -443,10 +459,6 @@
"defaultMessage": "It is now awaiting moderation.",
"description": "PageLayout: comment awaiting moderation"
},
- "VtYzuv": {
- "defaultMessage": "License:",
- "description": "ProjectsPage: license label"
- },
"WDwNDl": {
"defaultMessage": "Search",
"description": "SearchPage: SEO - Page title"
@@ -515,6 +527,10 @@
"defaultMessage": "Home",
"description": "Layout: main nav - home link"
},
+ "c0Oecl": {
+ "defaultMessage": "Created on:",
+ "description": "ProjectOverview: creation date label"
+ },
"c1Ju/q": {
"defaultMessage": "Leave a reply to {name}",
"description": "CommentsList: comment form title"
@@ -523,6 +539,10 @@
"defaultMessage": "Sidebar",
"description": "PageLayout: accessible name for the sidebar"
},
+ "cIAOyy": {
+ "defaultMessage": "Popularity:",
+ "description": "ProjectOverview: popularity label"
+ },
"dfTljv": {
"defaultMessage": "Main navigation",
"description": "Layout: main nav accessible name"
@@ -563,10 +583,6 @@
"defaultMessage": "{count} seconds",
"description": "useReadingTime: seconds count"
},
- "iDIKb7": {
- "defaultMessage": "Repositories:",
- "description": "ProjectsPage: repositories label"
- },
"iG5SHf": {
"defaultMessage": "{postTitle} cover",
"description": "PostPreview: an accessible name for the figure wrapping the cover"
@@ -691,10 +707,6 @@
"defaultMessage": "Gitlab profile",
"description": "ProjectsPage: Gitlab profile link"
},
- "sI7gJK": {
- "defaultMessage": "{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}",
- "description": "ProjectsPage: Github stars count"
- },
"sO/Iwj": {
"defaultMessage": "Contact me",
"description": "HomePage: contact button text"
@@ -759,10 +771,6 @@
"defaultMessage": "Updated on:",
"description": "ProjectsPage: update date label"
},
- "wVFA4m": {
- "defaultMessage": "Created on:",
- "description": "ProjectsPage: creation date label"
- },
"xYemkP": {
"defaultMessage": "Loading more articles...",
"description": "PostsList: loading more articles message"
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 27a3c8a..5d0fd21 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -87,6 +87,10 @@
"defaultMessage": "Merci. Votre message a été envoyé avec succès. J’y répondrai dès que possible.",
"description": "Contact: success message"
},
+ "3bKzk0": {
+ "defaultMessage": "Dépôts :",
+ "description": "ProjectOverview: repositories label"
+ },
"3f3PzH": {
"defaultMessage": "Github",
"description": "HomePage: Github link"
@@ -123,6 +127,10 @@
"defaultMessage": "Copier",
"description": "usePrism: copy button text (not clicked)"
},
+ "701ggm": {
+ "defaultMessage": "Illustration de {projectName}",
+ "description": "ProjectOverview: cover accessible name"
+ },
"75FYp7": {
"defaultMessage": "Profil Github",
"description": "ContactPage: Github profile link"
@@ -287,6 +295,10 @@
"defaultMessage": "En cours d’envoi…",
"description": "CommentForm: spinner message on submit"
},
+ "JbT+fA": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "ProjectOverview: update date label"
+ },
"Jm0a6H": {
"defaultMessage": "Profil Github",
"description": "CVPage: Github profile link"
@@ -315,10 +327,6 @@
"defaultMessage": "Page non trouvée",
"description": "Error404Page: page title"
},
- "KrNvQi": {
- "defaultMessage": "Popularité :",
- "description": "ProjectsPage: popularity label"
- },
"LszkU6": {
"defaultMessage": "Tous les articles dans {thematicName}",
"description": "ThematicPage: posts list heading"
@@ -359,10 +367,18 @@
"defaultMessage": "Publier",
"description": "CommentForm: submit button"
},
+ "OWkqXt": {
+ "defaultMessage": "Technologies :",
+ "description": "ProjectOverview: technologies label"
+ },
"OevMeU": {
"defaultMessage": "{minutesCount} minutes {secondsCount} secondes",
"description": "useReadingTime: minutes + seconds count"
},
+ "PBdVsm": {
+ "defaultMessage": "{starsCount, plural, =0 {0 étoile} one {# étoile} other {# étoiles}}",
+ "description": "ProjectOverview: stars count"
+ },
"PHO94k": {
"defaultMessage": "Aller à la page précédente",
"description": "PostsList: pagination backward link label"
@@ -391,6 +407,10 @@
"defaultMessage": "Retrouvez-moi ailleurs",
"description": "ContactPage: social media widget title"
},
+ "QtdnFV": {
+ "defaultMessage": "Licence :",
+ "description": "ProjectOverview: license label"
+ },
"R895yC": {
"defaultMessage": "CV",
"description": "Layout: main nav - cv link"
@@ -407,10 +427,6 @@
"defaultMessage": "Chargement de la popularité du dépôt…",
"description": "ProjectsPage: loading repository popularity"
},
- "RwNZ6p": {
- "defaultMessage": "Technologies :",
- "description": "ProjectsPage: technologies label"
- },
"Sm2wCk": {
"defaultMessage": "Profil LinkedIn",
"description": "CVPage: LinkedIn profile link"
@@ -443,10 +459,6 @@
"defaultMessage": "Il est maintenant en attente de modération.",
"description": "PageLayout: comment awaiting moderation"
},
- "VtYzuv": {
- "defaultMessage": "License :",
- "description": "ProjectsPage: license label"
- },
"WDwNDl": {
"defaultMessage": "Recherche",
"description": "SearchPage: SEO - Page title"
@@ -515,6 +527,10 @@
"defaultMessage": "Accueil",
"description": "Layout: main nav - home link"
},
+ "c0Oecl": {
+ "defaultMessage": "Créé le :",
+ "description": "ProjectOverview: creation date label"
+ },
"c1Ju/q": {
"defaultMessage": "Laisser une réponse à {name}",
"description": "CommentsList: comment form title"
@@ -523,6 +539,10 @@
"defaultMessage": "Barre latérale",
"description": "PageLayout: accessible name for the sidebar"
},
+ "cIAOyy": {
+ "defaultMessage": "Popularité :",
+ "description": "ProjectOverview: popularity label"
+ },
"dfTljv": {
"defaultMessage": "Navigation principale",
"description": "Layout: main nav accessible name"
@@ -563,10 +583,6 @@
"defaultMessage": "{count} secondes",
"description": "useReadingTime: seconds count"
},
- "iDIKb7": {
- "defaultMessage": "Dépôts :",
- "description": "ProjectsPage: repositories label"
- },
"iG5SHf": {
"defaultMessage": "Illustration de {postTitle}",
"description": "PostPreview: an accessible name for the figure wrapping the cover"
@@ -691,10 +707,6 @@
"defaultMessage": "Profil Gitlab",
"description": "ProjectsPage: Gitlab profile link"
},
- "sI7gJK": {
- "defaultMessage": "{starsCount, plural, =0 {0 étoile sur Github} one {# étoile sur Github} other {# étoiles sur Github}}",
- "description": "ProjectsPage: Github stars count"
- },
"sO/Iwj": {
"defaultMessage": "Me contacter",
"description": "HomePage: contact button text"
@@ -755,14 +767,6 @@
"defaultMessage": "Libre",
"description": "HomePage: link to free thematic"
},
- "wQrvgw": {
- "defaultMessage": "Mis à jour le :",
- "description": "ProjectsPage: update date label"
- },
- "wVFA4m": {
- "defaultMessage": "Créé le :",
- "description": "ProjectsPage: creation date label"
- },
"xYemkP": {
"defaultMessage": "Chargement des articles précédents…",
"description": "PostsList: loading more articles message"
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index d397d27..58c03ce 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -12,19 +12,19 @@ import {
Code,
getLayout,
Link,
- Overview,
PageLayout,
Sharing,
- SocialLink,
Spinner,
Heading,
List,
ListItem,
Figure,
type MetaItemData,
- type MetaValues,
Time,
Grid,
+ ProjectOverview,
+ type ProjectMeta,
+ type Repository,
} from '../../components';
import styles from '../../styles/pages/project.module.scss';
import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types';
@@ -204,49 +204,37 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
);
/**
- * Retrieve the repositories links.
+ * Retrieve the project repositories.
*
* @param {Repos} repositories - A repositories object.
- * @returns {MetaValues[]} - An array of meta values.
+ * @returns {Repository[]} - An array of repositories.
*/
- const getReposLinks = (repositories: Repos): MetaValues[] => {
- const links: MetaValues[] = [];
- const githubLabel = intl.formatMessage({
- defaultMessage: 'Github profile',
- description: 'ProjectsPage: Github profile link',
- id: 'Nx8Jo5',
- });
- const gitlabLabel = intl.formatMessage({
- defaultMessage: 'Gitlab profile',
- description: 'ProjectsPage: Gitlab profile link',
- id: 'sECHDg',
- });
+ const getRepos = (repositories: Repos): Repository[] => {
+ const definedRepos: Repository[] = [];
if (repositories.github)
- links.push({
- id: 'github',
- value: (
- <SocialLink
- icon="Github"
- label={githubLabel}
- url={repositories.github}
- />
- ),
+ definedRepos.push({
+ id: 'Github',
+ label: intl.formatMessage({
+ defaultMessage: 'Github profile',
+ description: 'ProjectsPage: Github profile link',
+ id: 'Nx8Jo5',
+ }),
+ url: repositories.github,
});
if (repositories.gitlab)
- links.push({
- id: 'gitlab',
- value: (
- <SocialLink
- icon="Gitlab"
- label={gitlabLabel}
- url={repositories.gitlab}
- />
- ),
+ definedRepos.push({
+ id: 'Gitlab',
+ label: intl.formatMessage({
+ defaultMessage: 'Gitlab profile',
+ description: 'ProjectsPage: Gitlab profile link',
+ id: 'sECHDg',
+ }),
+ url: repositories.gitlab,
});
- return links;
+ return definedRepos;
};
const loadingRepoPopularity = intl.formatMessage({
@@ -269,95 +257,19 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
if (isError) return 'Error';
if (isLoading || !data) return <Spinner aria-label={loadingRepoPopularity} />;
- const getRepoPopularity = (repo: string) => {
- const stars = intl.formatMessage(
- {
- defaultMessage:
- '{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}',
- description: 'ProjectsPage: Github stars count',
- id: 'sI7gJK',
- },
- { starsCount: data.stargazers_count }
- );
- const popularityUrl = `https://github.com/${repo}/stargazers`;
-
- return (
- <>
- ⭐&nbsp;
- <Link href={popularityUrl}>{stars}</Link>
- </>
- );
- };
-
- const overviewMeta: (MetaItemData | undefined)[] = [
- {
- id: 'creation-date',
- label: intl.formatMessage({
- defaultMessage: 'Created on:',
- description: 'ProjectsPage: creation date label',
- id: 'wVFA4m',
- }),
- value: <Time date={data.created_at} />,
- },
- {
- id: 'update-date',
- label: intl.formatMessage({
- defaultMessage: 'Updated on:',
- description: 'ProjectsPage: update date label',
- id: 'wQrvgw',
- }),
- value: <Time date={data.updated_at} />,
- },
- license
+ const overviewMeta: Partial<ProjectMeta> = {
+ creationDate: data.created_at,
+ lastUpdateDate: data.updated_at,
+ license,
+ popularity: repos?.github
? {
- id: 'license',
- label: intl.formatMessage({
- defaultMessage: 'License:',
- description: 'ProjectsPage: license label',
- id: 'VtYzuv',
- }),
- value: license,
+ count: data.stargazers_count,
+ url: `https://github.com/${repos.github}/stargazers`,
}
: undefined,
- repos?.github
- ? {
- id: 'popularity',
- label: intl.formatMessage({
- defaultMessage: 'Popularity:',
- description: 'ProjectsPage: popularity label',
- id: 'KrNvQi',
- }),
- value: getRepoPopularity(repos.github),
- }
- : undefined,
- repos
- ? {
- id: 'repositories',
- label: intl.formatMessage({
- defaultMessage: 'Repositories:',
- description: 'ProjectsPage: repositories label',
- id: 'iDIKb7',
- }),
- value: getReposLinks(repos),
- }
- : undefined,
- technologies
- ? {
- id: 'technologies',
- label: intl.formatMessage({
- defaultMessage: 'Technologies:',
- description: 'ProjectsPage: technologies label',
- id: 'RwNZ6p',
- }),
- value: technologies.map((techno) => {
- return { id: techno, value: techno };
- }),
- }
- : undefined,
- ];
- const filteredOverviewMeta = overviewMeta.filter(
- (item): item is MetaItemData => !!item
- );
+ repositories: repos ? getRepos(repos) : undefined,
+ technologies,
+ };
const webpageSchema = getWebPageSchema({
description: seo.description,
@@ -421,7 +333,11 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
/>,
]}
>
- <Overview cover={cover} meta={filteredOverviewMeta} />
+ <ProjectOverview
+ cover={cover ? <NextImage {...cover} /> : undefined}
+ meta={overviewMeta}
+ name={project.title}
+ />
<ProjectContent components={components} />
</PageLayout>
</>
diff --git a/src/types/generics.ts b/src/types/generics.ts
index 5377c54..6fb4e1d 100644
--- a/src/types/generics.ts
+++ b/src/types/generics.ts
@@ -3,3 +3,10 @@ export type Maybe<T> = T | undefined;
export type Nullable<T> = T | null;
export type DataValidator<T> = (data: T) => boolean | Promise<boolean>;
+
+export type ValueOf<
+ T extends Record<string, unknown>,
+ K extends keyof T = keyof T,
+> = {
+ [P in keyof T]: T[P];
+}[K];
diff --git a/tests/jest/__mocks__/svgr.mock.tsx b/tests/jest/__mocks__/svgr.mock.tsx
new file mode 100644
index 0000000..28fa414
--- /dev/null
+++ b/tests/jest/__mocks__/svgr.mock.tsx
@@ -0,0 +1,9 @@
+import React, { type SVGProps } from 'react';
+
+const SvgrMock = React.forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
+ (props, ref) => <svg ref={ref} {...props} />
+);
+SvgrMock.displayName = 'SvgrMock';
+
+export const ReactComponent = SvgrMock;
+export default SvgrMock;