diff options
| -rw-r--r-- | jest.config.js | 4 | ||||
| -rw-r--r-- | src/components/organisms/index.ts | 1 | ||||
| -rw-r--r-- | src/components/organisms/layout/index.ts | 1 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.module.scss | 37 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.stories.tsx | 78 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.test.tsx | 33 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.tsx | 44 | ||||
| -rw-r--r-- | src/components/organisms/project-overview/index.ts | 1 | ||||
| -rw-r--r-- | src/components/organisms/project-overview/project-overview.module.scss | 40 | ||||
| -rw-r--r-- | src/components/organisms/project-overview/project-overview.stories.tsx | 78 | ||||
| -rw-r--r-- | src/components/organisms/project-overview/project-overview.test.tsx | 127 | ||||
| -rw-r--r-- | src/components/organisms/project-overview/project-overview.tsx | 193 | ||||
| -rw-r--r-- | src/i18n/en.json | 56 | ||||
| -rw-r--r-- | src/i18n/fr.json | 60 | ||||
| -rw-r--r-- | src/pages/projets/[slug].tsx | 162 | ||||
| -rw-r--r-- | src/types/generics.ts | 7 | ||||
| -rw-r--r-- | tests/jest/__mocks__/svgr.mock.tsx | 9 |
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 ? ( + <> + ⭐ <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 ( - <> - ⭐ - <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; |
