From d375e5c9f162cbd84a6e6462977db56519d09f75 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Thu, 7 Dec 2023 18:48:53 +0100 Subject: refactor(pages): refine Project pages * refactor ProjectOverview component to let consumers handle the value * extract project overview depending on Github to avoid fetching Github API if the project is not on Github * wrap dynamic import in a useMemo hook to avoid infinite rerender * fix table of contents by adding a useMutationObserver hook to refresh headings tree (without it useHeadingsTree is not retriggered once the dynamic import is done) * add Cypress tests --- mdx.d.ts | 6 +- .../molecules/meta-list/meta-item/meta-item.tsx | 3 +- .../project-overview/project-overview.stories.tsx | 4 +- .../project-overview/project-overview.test.tsx | 64 ++-- .../project-overview/project-overview.tsx | 93 ++---- src/content | 2 +- src/i18n/en.json | 40 +-- src/i18n/fr.json | 40 +-- src/pages/article/[slug].tsx | 2 +- src/pages/cv.tsx | 2 +- src/pages/mentions-legales.tsx | 2 +- src/pages/projets/[slug].tsx | 356 ++++++++++++++------- src/pages/sujet/[slug].tsx | 2 +- src/pages/thematique/[slug].tsx | 2 +- src/styles/pages/project.module.scss | 11 - src/styles/pages/projects.module.scss | 14 + src/utils/constants.ts | 2 - src/utils/helpers/strings.ts | 6 +- src/utils/hooks/index.ts | 1 + .../use-headings-tree/use-headings-tree.test.ts | 54 ++-- .../hooks/use-headings-tree/use-headings-tree.ts | 50 ++- src/utils/hooks/use-mutation-observer/index.ts | 1 + .../use-mutation-observer.test.ts | 42 +++ .../use-mutation-observer/use-mutation-observer.ts | 35 ++ tests/cypress/e2e/pages/project.cy.ts | 33 ++ 25 files changed, 536 insertions(+), 331 deletions(-) delete mode 100644 src/styles/pages/project.module.scss create mode 100644 src/utils/hooks/use-mutation-observer/index.ts create mode 100644 src/utils/hooks/use-mutation-observer/use-mutation-observer.test.ts create mode 100644 src/utils/hooks/use-mutation-observer/use-mutation-observer.ts create mode 100644 tests/cypress/e2e/pages/project.cy.ts diff --git a/mdx.d.ts b/mdx.d.ts index 91cf7ea..47abfeb 100644 --- a/mdx.d.ts +++ b/mdx.d.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/consistent-type-imports */ declare module '*.mdx' { - type MDXProps = import('mdx/types').MDXProps; + type MDXContent = import('mdx/types').MDXContent; + type ComponentType = import('react').ComponentType; type MDXData = import('./src/types/data').MDXData; type MDXPageMeta = import('./src/types/data').MDXPageMeta; type MDXProjectMeta = import('./src/types/data').MDXProjectMeta; - const MDXComponent: (props: MDXProps) => JSX.Element; - export default MDXComponent; + export default ComponentType; export const data: MDXData; export const meta: MDXPageMeta | MDXProjectMeta; } diff --git a/src/components/molecules/meta-list/meta-item/meta-item.tsx b/src/components/molecules/meta-list/meta-item/meta-item.tsx index c5223c2..42a0801 100644 --- a/src/components/molecules/meta-list/meta-item/meta-item.tsx +++ b/src/components/molecules/meta-list/meta-item/meta-item.tsx @@ -1,13 +1,12 @@ import { type ForwardRefRenderFunction, - type ReactElement, type ReactNode, forwardRef, } from 'react'; import { Description, Group, type GroupProps, Term } from '../../../atoms'; import styles from './meta-item.module.scss'; -export type MetaValue = string | ReactElement; +export type MetaValue = ReactNode; export type MetaValues = { id: string; diff --git a/src/components/organisms/project-overview/project-overview.stories.tsx b/src/components/organisms/project-overview/project-overview.stories.tsx index 655dc3c..0c4138c 100644 --- a/src/components/organisms/project-overview/project-overview.stories.tsx +++ b/src/components/organisms/project-overview/project-overview.stories.tsx @@ -1,6 +1,6 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import NextImage from 'next/image'; -import { type ProjectMeta, ProjectOverview } from './project-overview'; +import { type OverviewMeta, ProjectOverview } from './project-overview'; /** * ProjectOverview - Storybook Meta @@ -49,7 +49,7 @@ const meta = { creationDate: '2015-09-02', lastUpdateDate: '2023-11-10', license: 'MIT', -} satisfies Partial; +} satisfies Partial; /** * ProjectOverview Stories - Meta diff --git a/src/components/organisms/project-overview/project-overview.test.tsx b/src/components/organisms/project-overview/project-overview.test.tsx index 6234368..f798a7b 100644 --- a/src/components/organisms/project-overview/project-overview.test.tsx +++ b/src/components/organisms/project-overview/project-overview.test.tsx @@ -1,13 +1,14 @@ 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'; +import { type OverviewMeta, ProjectOverview } from './project-overview'; +import { SocialLink } from 'src/components/atoms'; describe('ProjectOverview', () => { it('can render a meta for the creation date', () => { const meta = { creationDate: '2023-11-01', - } satisfies Partial; + } satisfies Partial; render(); @@ -17,7 +18,7 @@ describe('ProjectOverview', () => { it('can render a meta for the update date', () => { const meta = { lastUpdateDate: '2023-11-02', - } satisfies Partial; + } satisfies Partial; render(); @@ -27,7 +28,7 @@ describe('ProjectOverview', () => { it('can render a meta for the license', () => { const meta = { license: 'MIT', - } satisfies Partial; + } satisfies Partial; render(); @@ -37,38 +38,26 @@ describe('ProjectOverview', () => { it('can render a meta for the popularity', () => { const meta = { - popularity: { count: 5 }, - } satisfies Partial; + popularity: '5 stars', + } satisfies Partial; render(); 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; - - render(); - - expect(rtlScreen.getByRole('term')).toHaveTextContent('Popularity:'); - expect(rtlScreen.getByRole('definition')).toHaveTextContent( - `${meta.popularity.count} stars` - ); - expect(rtlScreen.getByRole('link')).toHaveAttribute( - 'href', - meta.popularity.url + meta.popularity ); }); it('can render a meta for the technologies', () => { const meta = { - technologies: ['Javascript', 'React'], - } satisfies Partial; + technologies: ['Javascript', 'React'].map((techno) => { + return { + id: techno, + value: techno, + }; + }), + } satisfies Partial; render(); @@ -79,9 +68,22 @@ describe('ProjectOverview', () => { }); it('can render a meta for the repositories', () => { + const repos = [{ id: 'Github' as const, label: 'Github', url: '#github' }]; const meta = { - repositories: [{ id: 'Github', label: 'Github', url: '#github' }], - } satisfies Partial; + repositories: repos.map((repo) => { + return { + id: repo.id, + value: ( + + ), + }; + }), + } satisfies Partial; render(); @@ -90,8 +92,8 @@ describe('ProjectOverview', () => { meta.repositories.length ); expect( - rtlScreen.getByRole('link', { name: meta.repositories[0].label }) - ).toHaveAttribute('href', meta.repositories[0].url); + rtlScreen.getByRole('link', { name: repos[0].label }) + ).toHaveAttribute('href', repos[0].url); }); it('can render a cover', () => { @@ -118,7 +120,7 @@ describe('ProjectOverview', () => { it('does not render a meta if the key is undefined', () => { const meta = { creationDate: undefined, - } satisfies Partial; + } satisfies Partial; render(); diff --git a/src/components/organisms/project-overview/project-overview.tsx b/src/components/organisms/project-overview/project-overview.tsx index f524120..d7416ec 100644 --- a/src/components/organisms/project-overview/project-overview.tsx +++ b/src/components/organisms/project-overview/project-overview.tsx @@ -6,35 +6,23 @@ import { type ReactElement, } from 'react'; import { useIntl } from 'react-intl'; -import type { ValueOf } from '../../../types'; +import { Figure } from '../../atoms'; import { - Time, - type SocialWebsite, - Link, - SocialLink, - Figure, -} from '../../atoms'; -import { MetaItem, type MetaItemProps, MetaList } from '../../molecules'; + MetaItem, + type MetaItemProps, + MetaList, + type MetaValue, + type MetaValues, +} from '../../molecules'; import styles from './project-overview.module.scss'; -export type Repository = { - id: Extract; - 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[]; +export type OverviewMeta = { + creationDate: MetaValue; + lastUpdateDate: MetaValue; + license: MetaValue; + popularity: MetaValue; + repositories: MetaValue | MetaValues[]; + technologies: MetaValues[]; }; const validMeta = [ @@ -44,9 +32,9 @@ const validMeta = [ 'popularity', 'repositories', 'technologies', -] satisfies (keyof ProjectMeta)[]; +] satisfies (keyof OverviewMeta)[]; -const isValidMetaKey = (key: string): key is keyof ProjectMeta => +const isValidMetaKey = (key: string): key is keyof OverviewMeta => (validMeta as string[]).includes(key); export type ProjectOverviewProps = Omit< @@ -60,7 +48,7 @@ export type ProjectOverviewProps = Omit< /** * The project meta. */ - meta: Partial; + meta: Partial; /** * The project name. */ @@ -112,48 +100,7 @@ const ProjectOverviewWithRef: ForwardRefRenderFunction< description: 'ProjectOverview: technologies label', id: 'OWkqXt', }), - } satisfies Record; - - const getMetaValue = useCallback( - (key: keyof ProjectMeta, value: ValueOf) => { - if (typeof value === 'string') { - return key === 'license' ? value :