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 --- src/pages/projets/[slug].tsx | 356 ++++++++++++++++++++++++++++--------------- 1 file changed, 232 insertions(+), 124 deletions(-) (limited to 'src/pages/projets') diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index cac6037..0c750f9 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -4,39 +4,49 @@ import type { GetStaticPaths, GetStaticProps } from 'next'; import dynamic from 'next/dynamic'; import Head from 'next/head'; import NextImage from 'next/image'; -import { useRouter } from 'next/router'; import Script from 'next/script'; -import type { ComponentType } from 'react'; +import { useMemo, type ComponentType, type FC } from 'react'; import { useIntl } from 'react-intl'; import { - getLayout, - SharingWidget, - Spinner, Heading, - ProjectOverview, - type ProjectMeta, - type Repository, + Link, + LoadingPage, + type MetaValues, Page, + PageBody, PageHeader, PageSidebar, + ProjectOverview, + SharingWidget, + SocialLink, + Spinner, + Time, TocWidget, - PageBody, + getLayout, + type ProjectOverviewProps, } from '../../components'; import { mdxComponents } from '../../components/mdx'; -import styles from '../../styles/pages/project.module.scss'; -import type { NextPageWithLayout, Project, Repos } from '../../types'; +import { fetchGithubRepoMeta } from '../../services/github'; +import styles from '../../styles/pages/projects.module.scss'; +import type { + GithubRepositoryMeta, + Maybe, + NextPageWithLayout, + Project, + ProjectMeta, +} from '../../types'; import { CONFIG } from '../../utils/config'; -import { GITHUB_PSEUDO, ROUTES } from '../../utils/constants'; import { + capitalize, getSchemaJson, getSinglePageSchema, getWebPageSchema, } from '../../utils/helpers'; import { + type Messages, getProjectData, getProjectFilenames, loadTranslation, - type Messages, } from '../../utils/helpers/server'; import { useBreadcrumb, @@ -44,139 +54,201 @@ import { useHeadingsTree, } from '../../utils/hooks'; -type ProjectPageProps = { - project: Project; - translation: Messages; -}; +const getGithubRepoInputFrom = (namespace: string) => { + const parts = namespace.split('/'); -/** - * Project page. - */ -const ProjectPage: NextPageWithLayout = ({ project }) => { - const { id, intro, meta, title } = project; - const { cover, dates, license, repos, seo, technologies } = meta; - const intl = useIntl(); - const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ - title, - url: `${ROUTES.PROJECTS}/${id}`, - }); - const { ref, tree } = useHeadingsTree({ fromLevel: 2 }); + if (parts.length !== 2) + throw new Error( + 'Invalid repo. It should use the following format: owner/name.' + ); - const ProjectContent: ComponentType = dynamic( - async () => import(`../../content/projects/${id}.mdx`), - { - loading: () => , - } - ); + return { owner: parts[0], name: parts[1] }; +}; - const { asPath } = useRouter(); - const page = { - title: `${seo.title} - ${CONFIG.name}`, - url: `${CONFIG.url}${asPath}`, +const isValidRepo = (name: string): name is 'github' | 'gitlab' => + ['github', 'gitlab'].includes(name); + +type GithubRepoOverviewProps = Omit< + ProjectOverviewProps, + 'cover' | 'meta' | 'name' +> & + Pick & { + repos: { + github: string; + gitlab?: string; + }; + title: string; }; - /** - * Retrieve the project repositories. - * - * @param {Repos} repositories - A repositories object. - * @returns {Repository[]} - An array of repositories. - */ - const getRepos = (repositories: Repos): Repository[] => { - const definedRepos: Repository[] = []; +const GithubRepoOverview: FC = ({ + cover, + license, + repos, + technologies, + title, + ...props +}) => { + const intl = useIntl(); + const { isLoading, meta: repoMeta } = useGithubRepoMeta( + getGithubRepoInputFrom(repos.github) + ); + const reposLabels = { + github: intl.formatMessage({ + defaultMessage: 'Github', + description: 'ProjectPage: Github repo label', + id: 'l82UU5', + }), + gitlab: intl.formatMessage({ + defaultMessage: 'Gitlab', + description: 'ProjectPage: Gitlab repo label', + id: '1msHuZ', + }), + }; + const stars = intl.formatMessage( + { + defaultMessage: + '{starsCount, plural, =0 {No stars} one {# star} other {# stars}}', + description: 'ProjectPage: stars count', + id: '4M71hp', + }, + { starsCount: repoMeta?.stargazerCount } + ); + const popularityURL = `https://github.com/${repos.github}/stargazers`; - if (repositories.github) - definedRepos.push({ - id: 'Github', - label: intl.formatMessage({ - defaultMessage: 'Github profile', - description: 'ProjectsPage: Github profile link', - id: 'Nx8Jo5', - }), - url: repositories.github, - }); + return isLoading ? ( + + {intl.formatMessage({ + defaultMessage: 'Loading the repository metadata...', + description: 'ProjectPage: loading repository metadata', + id: 'EET/tC', + })} + + ) : ( + : undefined} + meta={{ + creationDate: repoMeta?.createdAt ? ( +