diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-09-26 15:54:28 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-10-24 12:23:48 +0200 |
| commit | 70efcfeaa0603415dd992cb662d8efb960e6e49a (patch) | |
| tree | 5d37e98fae9aa7e5c3d8ef30a10db9fed9b63e36 /src | |
| parent | 31695306bfed44409f03006ea717fd2cceff8f87 (diff) | |
refactor(routes): replace hardcoded routes with constants
It makes it easier to change a route if needed and it avoid typo
mistakes.
I also refactored a bit the concerned files to be complient with the
new ESlint config. However, I should rewrite the pages to reduce
the number of statements.
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/templates/layout/layout.tsx | 60 | ||||
| -rw-r--r-- | src/i18n/en.json | 12 | ||||
| -rw-r--r-- | src/i18n/fr.json | 12 | ||||
| -rw-r--r-- | src/pages/404.tsx | 22 | ||||
| -rw-r--r-- | src/pages/article/[slug].tsx | 65 | ||||
| -rw-r--r-- | src/pages/blog/index.tsx | 71 | ||||
| -rw-r--r-- | src/pages/blog/page/[number].tsx | 51 | ||||
| -rw-r--r-- | src/pages/contact.tsx | 102 | ||||
| -rw-r--r-- | src/pages/cv.tsx | 133 | ||||
| -rw-r--r-- | src/pages/index.tsx | 129 | ||||
| -rw-r--r-- | src/pages/mentions-legales.tsx | 36 | ||||
| -rw-r--r-- | src/pages/projets/[slug].tsx | 134 | ||||
| -rw-r--r-- | src/pages/projets/index.tsx | 37 | ||||
| -rw-r--r-- | src/pages/recherche/index.tsx | 54 | ||||
| -rw-r--r-- | src/pages/sujet/[slug].tsx | 63 | ||||
| -rw-r--r-- | src/pages/thematique/[slug].tsx | 46 | ||||
| -rw-r--r-- | src/utils/constants.ts | 32 | ||||
| -rw-r--r-- | src/utils/helpers/pages.ts | 39 | ||||
| -rw-r--r-- | src/utils/helpers/rss.ts | 19 | ||||
| -rw-r--r-- | src/utils/helpers/schema-org.ts | 13 | ||||
| -rw-r--r-- | src/utils/hooks/use-breadcrumb.ts (renamed from src/utils/hooks/use-breadcrumb.tsx) | 24 |
21 files changed, 653 insertions, 501 deletions
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx index beb6562..7c97901 100644 --- a/src/components/templates/layout/layout.tsx +++ b/src/components/templates/layout/layout.tsx @@ -1,8 +1,16 @@ +/* eslint-disable max-statements */ import Script from 'next/script'; -import { FC, ReactElement, ReactNode, useRef, useState } from 'react'; +import { + type FC, + type ReactElement, + type ReactNode, + useRef, + useState, +} from 'react'; import { useIntl } from 'react-intl'; -import { Person, SearchAction, WebSite, WithContext } from 'schema-dts'; -import { type NextPageWithLayoutOptions } from '../../../types'; +import type { Person, SearchAction, WebSite, WithContext } from 'schema-dts'; +import type { NextPageWithLayoutOptions } from '../../../types'; +import { ROUTES } from '../../../utils/constants'; import { useRouteChange, useScrollPosition, @@ -25,7 +33,6 @@ import { Header, type HeaderProps, } from '../../organisms'; -import photo from '/public/armand-philippot.jpg'; import styles from './layout.module.scss'; export type QueryAction = SearchAction & { @@ -121,25 +128,25 @@ export const Layout: FC<LayoutProps> = ({ { id: 'blog', label: blogLabel, - href: '/blog', + href: ROUTES.BLOG, logo: <PostsStack aria-hidden={true} />, }, { id: 'projects', label: projectsLabel, - href: '/projets', + href: ROUTES.PROJECTS, logo: <ComputerScreen aria-hidden={true} />, }, { id: 'cv', label: cvLabel, - href: '/cv', + href: ROUTES.CV, logo: <Career aria-hidden={true} />, }, { id: 'contact', label: contactLabel, - href: '/contact', + href: ROUTES.CONTACT, logo: <Envelop aria-hidden={true} />, }, ]; @@ -151,14 +158,14 @@ export const Layout: FC<LayoutProps> = ({ }); const footerNav: FooterProps['navItems'] = [ - { id: 'legal-notice', label: legalNoticeLabel, href: '/mentions-legales' }, + { id: 'legal-notice', label: legalNoticeLabel, href: ROUTES.LEGAL_NOTICE }, ]; const searchActionSchema: QueryAction = { '@type': 'SearchAction', target: { '@type': 'EntryPoint', - urlTemplate: `${url}/recherche?s={search_term_string}`, + urlTemplate: `${url}${ROUTES.SEARCH}?s={search_term_string}`, }, query: 'required', 'query-input': 'required name=search_term_string', @@ -168,9 +175,9 @@ export const Layout: FC<LayoutProps> = ({ '@context': 'https://schema.org', '@id': `${url}`, '@type': 'WebSite', - name: name, + name, description: baseline, - url: url, + url, author: { '@id': `${url}/#branding` }, copyrightYear: Number(copyright.start), creator: { '@id': `${url}/#branding` }, @@ -183,10 +190,10 @@ export const Layout: FC<LayoutProps> = ({ '@context': 'https://schema.org', '@type': 'Person', '@id': `${url}/#branding`, - name: name, - url: url, + name, + url, jobTitle: baseline, - image: photo.src, + image: '/armand-philippot.jpg', subjectOf: { '@id': `${url}` }, }; @@ -194,48 +201,56 @@ export const Layout: FC<LayoutProps> = ({ styles['back-to-top--hidden'] ); const updateBackToTopClassName = () => { + const visibleBreakpoint = 300; setBackToTopClassName( - window.scrollY > 300 + window.scrollY > visibleBreakpoint ? styles['back-to-top--visible'] : styles['back-to-top--hidden'] ); }; useScrollPosition(updateBackToTopClassName); + const topRef = useRef<HTMLSpanElement>(null); const giveFocusToTopRef = () => { if (topRef.current) topRef.current.focus(); }; + useRouteChange(giveFocusToTopRef); return ( <> <Script dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-layout" type="application/ld+json" /> <Script dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }} + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-branding" type="application/ld+json" /> <noscript> - <div className={styles['noscript-spacing']}></div> + <div className={styles['noscript-spacing']} /> </noscript> - <span ref={topRef} tabIndex={-1}></span> + <span ref={topRef} tabIndex={-1} /> <ButtonLink target="#main" className="screen-reader-text"> {skipToContent} </ButtonLink> <Header + // eslint-disable-next-line react/jsx-no-literals -- Storage key allowed ackeeStorageKey="ackee-tracking" baseline={baseline} className={styles.header} isHome={isHome} + // eslint-disable-next-line react/jsx-no-literals -- Storage key allowed motionStorageKey="reduced-motion" nav={mainNav} - photo={photo} - searchPage="/recherche" + // eslint-disable-next-line react/jsx-no-literals -- Photo allowed + photo="/armand-philippot.jpg" + searchPage={ROUTES.SEARCH} title={name} withLink={true} /> @@ -254,6 +269,7 @@ export const Layout: FC<LayoutProps> = ({ topId="top" /> <noscript> + {/*eslint-disable-next-line react/jsx-no-literals -- Position allowed*/} <NoScript message={noScript} position="top" /> </noscript> </> @@ -270,6 +286,4 @@ export const Layout: FC<LayoutProps> = ({ export const getLayout = ( page: ReactElement, props: NextPageWithLayoutOptions -) => { - return <Layout {...props}>{page}</Layout>; -}; +) => <Layout {...props}>{page}</Layout>; diff --git a/src/i18n/en.json b/src/i18n/en.json index 02952b4..277ed23 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -143,6 +143,10 @@ "defaultMessage": "Contact", "description": "Layout: main nav - contact link" }, + "AN9iy7": { + "defaultMessage": "Contact", + "description": "ContactPage: page title" + }, "AuGklx": { "defaultMessage": "License:", "description": "Meta: license label" @@ -203,10 +207,6 @@ "defaultMessage": "Topics", "description": "Error404Page: topics list widget title" }, - "Gnf1Si": { - "defaultMessage": "{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}", - "description": "Projets: Github stars count" - }, "HFdzae": { "defaultMessage": "Contact form", "description": "ContactForm: form accessible name" @@ -559,6 +559,10 @@ "defaultMessage": "No comments.", "description": "PageLayout: no comments text" }, + "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" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 2ec3657..69f6b42 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -143,6 +143,10 @@ "defaultMessage": "Contact", "description": "Layout: main nav - contact link" }, + "AN9iy7": { + "defaultMessage": "Contact", + "description": "ContactPage: page title" + }, "AuGklx": { "defaultMessage": "Licence :", "description": "Meta: license label" @@ -203,10 +207,6 @@ "defaultMessage": "Sujets", "description": "Error404Page: topics list widget title" }, - "Gnf1Si": { - "defaultMessage": "{starsCount, plural, =0 {0 étoile sur Github} one {# étoile sur Github} other {# étoiles sur Github}}", - "description": "Projets: Github stars count" - }, "HFdzae": { "defaultMessage": "Formulaire de contact", "description": "ContactForm: form accessible name" @@ -559,6 +559,10 @@ "defaultMessage": "Aucun commentaire.", "description": "PageLayout: no comments text" }, + "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" diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 67daae1..af95a36 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,6 +1,6 @@ -import { GetStaticProps } from 'next'; +import type { GetStaticProps } from 'next'; import Head from 'next/head'; -import { ReactNode } from 'react'; +import type { ReactNode } from 'react'; import { useIntl } from 'react-intl'; import { getLayout, @@ -15,11 +15,12 @@ import { getTotalThematics, getTotalTopics, } from '../services/graphql'; -import { - type NextPageWithLayout, - type RawThematicPreview, - type RawTopicPreview, +import type { + NextPageWithLayout, + RawThematicPreview, + RawTopicPreview, } from '../types'; +import { ROUTES } from '../utils/constants'; import { getLinksListItems, getPageLinkFromRawData } from '../utils/helpers'; import { loadTranslation, type Messages } from '../utils/helpers/server'; import { useBreadcrumb, useSettings } from '../utils/hooks'; @@ -52,12 +53,12 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ description: 'Error404Page: page body', }, { - link: (chunks: ReactNode) => <Link href="/contact">{chunks}</Link>, + link: (chunks: ReactNode) => <Link href={ROUTES.CONTACT}>{chunks}</Link>, } ); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/404`, + url: ROUTES.NOT_FOUND, }); const pageTitle = intl.formatMessage( { @@ -88,6 +89,7 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ <> <Head> <title>{pageTitle}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={pageDescription} /> </Head> <PageLayout @@ -96,6 +98,7 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ breadcrumbSchema={breadcrumbSchema} widgets={[ <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="thematics-list" items={getLinksListItems( thematicsList.map((thematic) => @@ -106,6 +109,7 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ level={2} />, <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="topics-list" items={getLinksListItems( topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic')) @@ -123,7 +127,7 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ id: 'XKy7rx', })} </p> - <SearchForm isLabelHidden searchPage="/recherche/" /> + <SearchForm isLabelHidden searchPage={ROUTES.SEARCH} /> </PageLayout> </> ); diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index f564f35..9ecd8e1 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -1,9 +1,10 @@ -import { GetStaticPaths, GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { ParsedUrlQuery } from 'querystring'; +import type { GetStaticPaths, GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import { ParsedUrlQuery } from 'querystring'; -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import { useIntl } from 'react-intl'; import { ButtonLink, @@ -21,11 +22,8 @@ import { getArticleBySlug, } from '../../services/graphql'; import styles from '../../styles/pages/article.module.scss'; -import { - type Article, - type NextPageWithLayout, - type SingleComment, -} from '../../types'; +import type { Article, NextPageWithLayout, SingleComment } from '../../types'; +import { ROUTES } from '../../utils/constants'; import { getBlogSchema, getSchemaJson, @@ -66,17 +64,17 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ fallback: comments, }); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ - title: article?.title || '', - url: `/article/${slug}`, + title: article?.title ?? '', + url: `${ROUTES.ARTICLE}/${slug}`, }); - const readingTime = useReadingTime(article?.meta.wordsCount || 0, true); + const readingTime = useReadingTime(article?.meta.wordsCount ?? 0, true); const { website } = useSettings(); const prismPlugins: OptionalPrismPlugin[] = ['command-line', 'line-numbers']; const { attributes, className } = usePrism({ plugins: prismPlugins }); - if (isFallback) return <Spinner />; + if (isFallback || !article) return <Spinner />; - const { content, id, intro, meta, title } = article!; + const { content, id, intro, meta, title } = article; const { author, commentsCount, cover, dates, seo, thematics, topics } = meta; const headerMeta: PageLayoutProps['headerMeta'] = { @@ -87,13 +85,13 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ ? { date: dates.update } : undefined, readingTime, - thematics: - thematics && - thematics.map((thematic) => ( - <Link key={thematic.id} href={thematic.url}> - {thematic.name} - </Link> - )), + thematics: thematics + ? thematics.map((thematic) => ( + <Link key={thematic.id} href={thematic.url}> + {thematic.name} + </Link> + )) + : undefined, }; const footerMetaLabel = intl.formatMessage({ @@ -105,13 +103,11 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ const footerMeta: PageLayoutProps['footerMeta'] = { custom: topics && { label: footerMetaLabel, - value: topics.map((topic) => { - return ( - <ButtonLink key={topic.id} target={topic.url} className={styles.btn}> - {topic.logo && <ResponsiveImage {...topic.logo} />} {topic.name} - </ButtonLink> - ); - }), + value: topics.map((topic) => ( + <ButtonLink key={topic.id} target={topic.url} className={styles.btn}> + {topic.logo ? <ResponsiveImage {...topic.logo} /> : null} {topic.name} + </ButtonLink> + )), }, }; @@ -160,7 +156,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ */ const prismClassNameReplacer = (str: string): string => { const wpBlockClassName = 'wp-block-code'; - const languageArray = str.match(/language-[^\s|"]+/); + const languageArray = /language-[^\s|"]+/.exec(str); const languageClassName = languageArray ? `${languageArray[0]}` : ''; if ( @@ -184,15 +180,19 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ <> <Head> <title>{seo.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${pageUrl}`} /> + <meta property="og:url" content={pageUrl} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> <meta property="og:title" content={title} /> <meta property="og:description" content={intro} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-project" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout @@ -212,6 +212,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ withToC={true} widgets={[ <Sharing + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="sharing-widget" className={styles.widget} data={{ excerpt: intro, title, url: pageUrl }} @@ -234,15 +235,15 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ ArticlePage.getLayout = (page) => getLayout(page, { useGrid: true }); -interface PostParams extends ParsedUrlQuery { +type PostParams = { slug: string; -} +} & ParsedUrlQuery; export const getStaticProps: GetStaticProps<ArticlePageProps> = async ({ locale, params, }) => { - const post = await getArticleBySlug(params!.slug as PostParams['slug']); + const post = await getArticleBySlug((params as PostParams).slug); const comments = await getAllComments({ contentId: post.id as number }); const translation = await loadTranslation(locale); diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx index 13a4c57..7f6c540 100644 --- a/src/pages/blog/index.tsx +++ b/src/pages/blog/index.tsx @@ -1,7 +1,9 @@ -import { GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; +import { useCallback } from 'react'; import { useIntl } from 'react-intl'; import { getLayout, @@ -18,14 +20,15 @@ import { getTotalThematics, getTotalTopics, } from '../../services/graphql'; -import { - type EdgesResponse, - type NextPageWithLayout, - type RawArticle, - type RawThematicPreview, - type RawTopicPreview, +import type { + EdgesResponse, + NextPageWithLayout, + RawArticle, + RawThematicPreview, + RawTopicPreview, } from '../../types'; import { settings } from '../../utils/config'; +import { ROUTES } from '../../utils/constants'; import { getBlogSchema, getLinksListItems, @@ -62,19 +65,22 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ }); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: '/blog', + url: ROUTES.BLOG, }); const { blog, website } = useSettings(); const { asPath } = useRouter(); - const pageTitle = intl.formatMessage( - { - defaultMessage: 'Blog: development, open source - {websiteName}', - description: 'BlogPage: SEO - Page title', - id: '+Y+tLK', - }, - { websiteName: website.name } - ); + const page = { + title: intl.formatMessage( + { + defaultMessage: 'Blog: development, open source - {websiteName}', + description: 'BlogPage: SEO - Page title', + id: '+Y+tLK', + }, + { websiteName: website.name } + ), + url: `${website.url}${asPath}`, + }; const pageDescription = intl.formatMessage( { defaultMessage: @@ -110,12 +116,9 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ perPage: blog.postsPerPage, }); - /** - * Load more posts handler. - */ - const loadMore = () => { + const loadMore = useCallback(() => { setSize((prevSize) => prevSize + 1); - }; + }, [setSize]); const thematicsListTitle = intl.formatMessage({ defaultMessage: 'Thematics', @@ -128,20 +131,25 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ description: 'BlogPage: topics list widget title', id: '2D9tB5', }); + const postsListBaseUrl = `${ROUTES.BLOG}/page/`; return ( <> <Head> - <title>{pageTitle}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={pageDescription} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="website" /> <meta property="og:title" content={title} /> <meta property="og:description" content={pageDescription} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-blog" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout @@ -151,6 +159,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ headerMeta={{ total: totalArticles }} widgets={[ <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="thematics-list" items={getLinksListItems( thematicsList.map((thematic) => @@ -161,6 +170,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ level={2} />, <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="topics-list" items={getLinksListItems( topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic')) @@ -170,20 +180,21 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ />, ]} > - {data && ( + {data ? ( <PostsList - baseUrl="/blog/page/" + baseUrl={postsListBaseUrl} byYear={true} - isLoading={isLoadingMore || isLoadingInitialData} + isLoading={isLoadingMore ?? isLoadingInitialData} loadMore={loadMore} posts={getPostsList(data)} - searchPage="/recherche/" + searchPage={ROUTES.SEARCH} showLoadMoreBtn={hasNextPage} total={totalArticles} /> - )} - {error && ( + ) : null} + {error ? ( <Notice + // eslint-disable-next-line react/jsx-no-literals -- Kind allowed kind="error" message={intl.formatMessage({ defaultMessage: 'Failed to load.', @@ -191,7 +202,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ id: 'C/XGkH', })} /> - )} + ) : null} </PageLayout> </> ); diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx index 4eaade5..b63fa9b 100644 --- a/src/pages/blog/page/[number].tsx +++ b/src/pages/blog/page/[number].tsx @@ -1,8 +1,9 @@ -import { GetStaticPaths, GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { ParsedUrlQuery } from 'querystring'; +import type { GetStaticPaths, GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import { ParsedUrlQuery } from 'querystring'; import { useIntl } from 'react-intl'; import { getLayout, @@ -19,12 +20,12 @@ import { getTotalThematics, getTotalTopics, } from '../../../services/graphql'; -import { - type EdgesResponse, - type NextPageWithLayout, - type RawArticle, - type RawThematicPreview, - type RawTopicPreview, +import type { + EdgesResponse, + NextPageWithLayout, + RawArticle, + RawThematicPreview, + RawTopicPreview, } from '../../../types'; import { settings } from '../../../utils/config'; import { @@ -41,6 +42,7 @@ import { useRedirection, useSettings, } from '../../../utils/hooks'; +import { ROUTES } from 'src/utils/constants'; type BlogPageProps = { articles: EdgesResponse<RawArticle>; @@ -63,7 +65,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ }) => { useRedirection({ query: { param: 'number', value: '1' }, - redirectTo: '/blog', + redirectTo: ROUTES.BLOG, }); const intl = useIntl(); @@ -85,12 +87,15 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ const pageTitleWithPageNumber = `${title} - ${pageNumberTitle}`; const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title: pageNumberTitle, - url: `/blog/page/${pageNumber}`, + url: `${ROUTES.BLOG}/page/${pageNumber}`, }); const { website } = useSettings(); const { asPath } = useRouter(); - const pageTitle = `${pageTitleWithPageNumber} - ${website.name}`; + const page = { + title: `${pageTitleWithPageNumber} - ${website.name}`, + url: `${website.url}${asPath}`, + }; const pageDescription = intl.formatMessage( { defaultMessage: @@ -124,20 +129,25 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ description: 'BlogPage: topics list widget title', id: '2D9tB5', }); + const postsListBaseUrl = `${ROUTES.BLOG}/page/`; return ( <> <Head> - <title>{pageTitle}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={pageDescription} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="website" /> <meta property="og:title" content={pageTitleWithPageNumber} /> <meta property="og:description" content={pageDescription} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-blog" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout @@ -147,6 +157,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ headerMeta={{ total: totalArticles }} widgets={[ <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="thematics-list" items={getLinksListItems( thematicsList.map((thematic) => @@ -157,6 +168,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ level={2} />, <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="topics-list" items={getLinksListItems( topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic')) @@ -167,11 +179,11 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ ]} > <PostsList - baseUrl="/blog/page/" + baseUrl={postsListBaseUrl} byYear={true} pageNumber={pageNumber} posts={getPostsList([articles])} - searchPage="/recherche/" + searchPage={ROUTES.SEARCH} total={totalArticles} /> </PageLayout> @@ -182,18 +194,17 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ BlogPage.getLayout = (page) => getLayout(page, { useGrid: true, withExtraPadding: true }); -interface BlogPageParams extends ParsedUrlQuery { +type BlogPageParams = { number: string; -} +} & ParsedUrlQuery; export const getStaticProps: GetStaticProps<BlogPageProps> = async ({ locale, params, }) => { - const pageNumber = Number(params!.number as BlogPageParams['number']); - const queriedPostsNumber = settings.postsPerPage * pageNumber; + const pageNumber = Number((params as BlogPageParams).number); const lastCursor = await getArticlesEndCursor({ - first: queriedPostsNumber, + first: settings.postsPerPage * pageNumber, }); const articles = await getArticles({ first: settings.postsPerPage, diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx index 92c58cc..d187a93 100644 --- a/src/pages/contact.tsx +++ b/src/pages/contact.tsx @@ -1,8 +1,9 @@ -import { GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useIntl } from 'react-intl'; import { ContactForm, @@ -16,7 +17,8 @@ import { import { meta } from '../content/pages/contact.mdx'; import { sendMail } from '../services/graphql'; import styles from '../styles/pages/contact.module.scss'; -import { type NextPageWithLayout } from '../types'; +import type { NextPageWithLayout } from '../types'; +import { ROUTES } from '../utils/constants'; import { getSchemaJson, getSinglePageSchema, @@ -30,9 +32,14 @@ const ContactPage: NextPageWithLayout = () => { const intl = useIntl(); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/contact`, + url: ROUTES.CONTACT, }); + const pageTitle = intl.formatMessage({ + defaultMessage: 'Contact', + description: 'ContactPage: page title', + id: 'AN9iy7', + }); const socialMediaTitle = intl.formatMessage({ defaultMessage: 'Find me elsewhere', description: 'ContactPage: social media widget title', @@ -61,6 +68,7 @@ const ContactPage: NextPageWithLayout = () => { const widgets = [ <SocialMedia + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="social-media" title={socialMediaTitle} level={2} @@ -75,56 +83,66 @@ const ContactPage: NextPageWithLayout = () => { />, ]; - const [status, setStatus] = useState<NoticeKind>('info'); + const [statusKind, setStatusKind] = useState<NoticeKind>('info'); const [statusMessage, setStatusMessage] = useState<string>(''); - const submitMail: ContactFormProps['sendMail'] = async (data, reset) => { - const { email, message, name, object } = data; - const messageHTML = message.replace(/\r?\n/g, '<br />'); - const body = `Message received from ${name} <${email}> on ${website.url}.<br /><br />${messageHTML}`; - const replyTo = `${name} <${email}>`; - const mailData = { - body, - clientMutationId: 'contact', - replyTo, - subject: object, - }; - const { message: mutationMessage, sent } = await sendMail(mailData); + const submitMail: ContactFormProps['sendMail'] = useCallback( + async (data, reset) => { + const { email, message, name, object } = data; + const messageHTML = message.replace(/\r?\n/g, '<br />'); + const body = `Message received from ${name} <${email}> on ${website.url}.<br /><br />${messageHTML}`; + const replyTo = `${name} <${email}>`; + const mailData = { + body, + clientMutationId: 'contact', + replyTo, + subject: object, + }; + const { message: mutationMessage, sent } = await sendMail(mailData); - if (sent) { - setStatus('success'); - setStatusMessage( - intl.formatMessage({ - defaultMessage: - 'Thanks. Your message was successfully sent. I will answer it as soon as possible.', - description: 'Contact: success message', - id: '3Pipok', - }) - ); - reset(); - } else { - const errorPrefix = intl.formatMessage({ - defaultMessage: 'An error occurred:', - description: 'Contact: error message', - id: 'TpyFZ6', - }); - const error = `${errorPrefix} ${mutationMessage}`; - setStatus('error'); - setStatusMessage(error); - } + if (sent) { + setStatusKind('success'); + setStatusMessage( + intl.formatMessage({ + defaultMessage: + 'Thanks. Your message was successfully sent. I will answer it as soon as possible.', + description: 'Contact: success message', + id: '3Pipok', + }) + ); + reset(); + } else { + const errorPrefix = intl.formatMessage({ + defaultMessage: 'An error occurred:', + description: 'Contact: error message', + id: 'TpyFZ6', + }); + const error = `${errorPrefix} ${mutationMessage}`; + setStatusKind('error'); + setStatusMessage(error); + } + }, + [intl, website.url] + ); + const page = { + title: `${seo.title} - ${website.name}`, + url: `${website.url}${asPath}`, }; return ( <> <Head> - <title>{`${seo.title} - ${website.name}`}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> <meta property="og:title" content={title} /> <meta property="og:description" content={intro} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-contact" type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} @@ -133,7 +151,7 @@ const ContactPage: NextPageWithLayout = () => { breadcrumb={breadcrumbItems} breadcrumbSchema={breadcrumbSchema} intro={intro} - title="Contact" + title={pageTitle} widgets={widgets} > <ContactForm @@ -141,7 +159,7 @@ const ContactPage: NextPageWithLayout = () => { Notice={ statusMessage ? ( <Notice - kind={status} + kind={statusKind} message={statusMessage} className={styles.notice} /> diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx index 9e1e7db..3910d61 100644 --- a/src/pages/cv.tsx +++ b/src/pages/cv.tsx @@ -1,9 +1,14 @@ -import { MDXComponents } from 'mdx/types'; -import { GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { MDXComponents } from 'mdx/types'; +import type { GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import React, { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from 'react'; +import React, { + type AnchorHTMLAttributes, + type HTMLAttributes, + type ReactNode, +} from 'react'; import { useIntl } from 'react-intl'; import { getLayout, @@ -12,12 +17,13 @@ import { Link, List, PageLayout, - type PageLayoutProps, SocialMedia, + type MetaData, } from '../components'; import CVContent, { data, meta } from '../content/pages/cv.mdx'; import styles from '../styles/pages/cv.module.scss'; -import { type NextPageWithLayout } from '../types'; +import type { NextPageWithLayout } from '../types'; +import { PERSONAL_LINKS, ROUTES } from '../utils/constants'; import { getSchemaJson, getSinglePageSchema, @@ -39,67 +45,67 @@ const ExternalLink = ({ const H1 = ({ children = '', ...props -}: HTMLAttributes<HTMLHeadingElement>) => { - return ( - <Heading {...props} level={1}> - {children} - </Heading> - ); -}; +}: HTMLAttributes<HTMLHeadingElement>) => ( + <Heading {...props} level={1}> + {children} + </Heading> +); const H2 = ({ children = '', ...props -}: HTMLAttributes<HTMLHeadingElement>) => { - return ( - <Heading {...props} level={2}> - {children} - </Heading> - ); -}; +}: HTMLAttributes<HTMLHeadingElement>) => ( + <Heading {...props} level={2}> + {children} + </Heading> +); const H3 = ({ children = '', ...props -}: HTMLAttributes<HTMLHeadingElement>) => { - return ( - <Heading {...props} level={3}> - {children} - </Heading> - ); -}; +}: HTMLAttributes<HTMLHeadingElement>) => ( + <Heading {...props} level={3}> + {children} + </Heading> +); const H4 = ({ children = '', ...props -}: HTMLAttributes<HTMLHeadingElement>) => { - return ( - <Heading {...props} level={4}> - {children} - </Heading> - ); -}; +}: HTMLAttributes<HTMLHeadingElement>) => ( + <Heading {...props} level={4}> + {children} + </Heading> +); const H5 = ({ children = '', ...props -}: HTMLAttributes<HTMLHeadingElement>) => { - return ( - <Heading {...props} level={5}> - {children} - </Heading> - ); -}; +}: HTMLAttributes<HTMLHeadingElement>) => ( + <Heading {...props} level={5}> + {children} + </Heading> +); const H6 = ({ children = '', ...props -}: HTMLAttributes<HTMLHeadingElement>) => { - return ( - <Heading {...props} level={6}> - {children} - </Heading> - ); +}: HTMLAttributes<HTMLHeadingElement>) => ( + <Heading {...props} level={6}> + {children} + </Heading> +); + +const components: MDXComponents = { + a: ExternalLink, + h1: H1, + h2: H2, + h3: H3, + h4: H4, + h5: H5, + h6: H6, + Link, + List, }; /** @@ -111,7 +117,7 @@ const CVPage: NextPageWithLayout = () => { const { dates, intro, seo, title } = meta; const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/cv`, + url: ROUTES.CV, }); const imageWidgetTitle = intl.formatMessage({ @@ -125,7 +131,7 @@ const CVPage: NextPageWithLayout = () => { id: '+Dre5J', }); - const headerMeta: PageLayoutProps['headerMeta'] = { + const headerMeta: MetaData = { publication: { date: dates.publication, }, @@ -154,6 +160,7 @@ const CVPage: NextPageWithLayout = () => { const widgets = [ <ImageWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="image-widget" expanded={true} title={imageWidgetTitle} @@ -163,15 +170,16 @@ const CVPage: NextPageWithLayout = () => { imageClassName={styles.image} />, <SocialMedia + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="social-media" title={socialMediaTitle} level={2} media={[ - { name: 'Github', url: 'https://github.com/ArmandPhilippot' }, - { name: 'Gitlab', url: 'https://gitlab.com/ArmandPhilippot' }, + { name: 'Github', url: PERSONAL_LINKS.GITHUB }, + { name: 'Gitlab', url: PERSONAL_LINKS.GITLAB }, { name: 'LinkedIn', - url: 'https://www.linkedin.com/in/armandphilippot', + url: PERSONAL_LINKS.LINKEDIN, }, ]} />, @@ -193,20 +201,12 @@ const CVPage: NextPageWithLayout = () => { kind: 'about', locale: website.locales.default, slug: asPath, - title: title, + title, }); const schemaJsonLd = getSchemaJson([webpageSchema, cvSchema]); - - const components: MDXComponents = { - a: ExternalLink, - h1: H1, - h2: H2, - h3: H3, - h4: H4, - h5: H5, - h6: H6, - Link, - List, + const page = { + title: `${seo.title} - ${website.name}`, + url: `${website.url}${asPath}`, }; return ( @@ -220,9 +220,11 @@ const CVPage: NextPageWithLayout = () => { withToC={true} > <Head> - <title>{`${seo.title} - ${website.name}`}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> <meta property="og:title" content={title} /> <meta property="og:description" content={intro} /> @@ -230,6 +232,7 @@ const CVPage: NextPageWithLayout = () => { <meta property="og:image:alt" content={title} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-cv" type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 8c357f1..9cecfcf 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,8 +1,8 @@ -import { MDXComponents } from 'mdx/types'; -import { GetStaticProps } from 'next'; +import type { MDXComponents } from 'mdx/types'; +import type { GetStaticProps } from 'next'; import Head from 'next/head'; import Script from 'next/script'; -import { ReactNode, isValidElement } from 'react'; +import { type FC, type ReactNode, isValidElement } from 'react'; import { useIntl } from 'react-intl'; import FeedIcon from '../assets/images/icon-feed.svg'; import { @@ -23,7 +23,8 @@ import { import HomePageContent from '../content/pages/homepage.mdx'; import { getArticlesCard } from '../services/graphql'; import styles from '../styles/pages/home.module.scss'; -import { type ArticleCard, type NextPageWithLayout } from '../types'; +import type { ArticleCard, NextPageWithLayout } from '../types'; +import { PERSONAL_LINKS, ROUTES } from '../utils/constants'; import { getSchemaJson, getWebPageSchema } from '../utils/helpers'; import { loadTranslation, type Messages } from '../utils/helpers/server'; import { useBreadcrumb, useSettings } from '../utils/hooks'; @@ -33,13 +34,13 @@ import { useBreadcrumb, useSettings } from '../utils/hooks'; * * @returns {JSX.Element} - A list of links. */ -const CodingLinks = (): JSX.Element => { +const CodingLinks: FC = () => { const intl = useIntl(); const links: ListItem[] = [ { id: 'web-development', value: ( - <ButtonLink target="/thematique/developpement-web"> + <ButtonLink target={ROUTES.THEMATICS.WEB_DEV}> {intl.formatMessage({ defaultMessage: 'Web development', description: 'HomePage: link to web development thematic', @@ -51,7 +52,7 @@ const CodingLinks = (): JSX.Element => { { id: 'projects', value: ( - <ButtonLink target="/projets"> + <ButtonLink target={ROUTES.PROJECTS}> {intl.formatMessage({ defaultMessage: 'Projects', description: 'HomePage: link to projects', @@ -62,6 +63,7 @@ const CodingLinks = (): JSX.Element => { }, ]; + // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed return <List kind="flex" items={links} className={styles.list} />; }; @@ -70,16 +72,17 @@ const CodingLinks = (): JSX.Element => { * * @returns {JSX.Element} - A list of links. */ -const ColdarkRepos = (): JSX.Element => { +const ColdarkRepos: FC = () => { const intl = useIntl(); + const repo = { + github: 'https://github.com/ArmandPhilippot/coldark', + gitlab: 'https://gitlab.com/ArmandPhilippot/coldark', + }; const links: ListItem[] = [ { id: 'coldark-github', value: ( - <ButtonLink - target="https://github.com/ArmandPhilippot/coldark" - external={true} - > + <ButtonLink target={repo.github} external={true}> {intl.formatMessage({ defaultMessage: 'Github', description: 'HomePage: Github link', @@ -91,10 +94,7 @@ const ColdarkRepos = (): JSX.Element => { { id: 'coldark-gitlab', value: ( - <ButtonLink - target="https://gitlab.com/ArmandPhilippot/coldark" - external={true} - > + <ButtonLink target={repo.gitlab} external={true}> {intl.formatMessage({ defaultMessage: 'Gitlab', description: 'HomePage: Gitlab link', @@ -105,6 +105,7 @@ const ColdarkRepos = (): JSX.Element => { }, ]; + // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed return <List kind="flex" items={links} className={styles.list} />; }; @@ -113,13 +114,13 @@ const ColdarkRepos = (): JSX.Element => { * * @returns {JSX.Element} - A list of links. */ -const LibreLinks = (): JSX.Element => { +const LibreLinks: FC = () => { const intl = useIntl(); const links: ListItem[] = [ { id: 'free', value: ( - <ButtonLink target="/thematique/libre"> + <ButtonLink target={ROUTES.THEMATICS.FREE}> {intl.formatMessage({ defaultMessage: 'Free', description: 'HomePage: link to free thematic', @@ -131,7 +132,7 @@ const LibreLinks = (): JSX.Element => { { id: 'linux', value: ( - <ButtonLink target="/thematique/linux"> + <ButtonLink target={ROUTES.THEMATICS.LINUX}> {intl.formatMessage({ defaultMessage: 'Linux', description: 'HomePage: link to Linux thematic', @@ -142,6 +143,7 @@ const LibreLinks = (): JSX.Element => { }, ]; + // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed return <List kind="flex" items={links} className={styles.list} />; }; @@ -150,13 +152,14 @@ const LibreLinks = (): JSX.Element => { * * @returns {JSX.Element} - A list of links */ -const ShaarliLink = (): JSX.Element => { +const ShaarliLink: FC = () => { const intl = useIntl(); + const shaarliUrl = PERSONAL_LINKS.SHAARLI; const links: ListItem[] = [ { id: 'shaarli', value: ( - <ButtonLink target="https://shaarli.armandphilippot.com/"> + <ButtonLink target={shaarliUrl}> {intl.formatMessage({ defaultMessage: 'Shaarli', description: 'HomePage: link to Shaarli', @@ -167,6 +170,7 @@ const ShaarliLink = (): JSX.Element => { }, ]; + // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed return <List kind="flex" items={links} className={styles.list} />; }; @@ -175,13 +179,14 @@ const ShaarliLink = (): JSX.Element => { * * @returns {JSX.Element} - A list of links. */ -const MoreLinks = (): JSX.Element => { +const MoreLinks: FC = () => { const intl = useIntl(); + const feedIconClass = `${styles.icon} ${styles['icon--feed']}`; const links: ListItem[] = [ { id: 'contact-me', value: ( - <ButtonLink target="/contact"> + <ButtonLink target={ROUTES.CONTACT}> <Envelop aria-hidden={true} className={styles.icon} /> {intl.formatMessage({ defaultMessage: 'Contact me', @@ -194,11 +199,8 @@ const MoreLinks = (): JSX.Element => { { id: 'rss-feed', value: ( - <ButtonLink target="/feed"> - <FeedIcon - aria-hidden={true} - className={`${styles.icon} ${styles['icon--feed']}`} - /> + <ButtonLink target={ROUTES.RSS}> + <FeedIcon aria-hidden={true} className={feedIconClass} /> {intl.formatMessage({ defaultMessage: 'Subscribe', description: 'HomePage: RSS feed subscription text', @@ -209,11 +211,38 @@ const MoreLinks = (): JSX.Element => { }, ]; + // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed return <List kind="flex" items={links} className={styles.list} />; }; -const StyledColumns = (props: ColumnsProps) => { - return <Columns className={styles.columns} {...props} />; +const StyledColumns = (props: ColumnsProps) => ( + <Columns className={styles.columns} {...props} /> +); + +/** + * Create the page sections. + * + * @param {object} obj - An object containing the section body. + * @param {ReactNode[]} obj.children - The section body. + * @returns {JSX.Element} A section element. + */ +const getSection = ({ + children, + variant, +}: { + children: ReactNode[]; + variant: SectionProps['variant']; +}): JSX.Element => { + const [headingEl, ...content] = children; + + return ( + <Section + className={styles.section} + content={content} + title={isValidElement(headingEl) ? headingEl.props.children : ''} + variant={variant} + /> + ); }; type HomeProps = { @@ -243,43 +272,12 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => { id: post.slug, meta: { publication: { date: post.dates.publication } }, title: post.title, - url: `/article/${post.slug}`, + url: `${ROUTES.ARTICLE}/${post.slug}`, }; }); + const listClass = `${styles.list} ${styles['list--cards']}`; - return ( - <CardsList - items={posts} - titleLevel={3} - className={`${styles.list} ${styles['list--cards']}`} - /> - ); - }; - - /** - * Create the page sections. - * - * @param {object} obj - An object containing the section body. - * @param {ReactNode[]} obj.children - The section body. - * @returns {JSX.Element} A section element. - */ - const getSection = ({ - children, - variant, - }: { - children: ReactNode[]; - variant: SectionProps['variant']; - }): JSX.Element => { - const [headingEl, ...content] = children; - - return ( - <Section - className={styles.section} - content={content} - title={isValidElement(headingEl) ? headingEl.props.children : ''} - variant={variant} - /> - ); + return <CardsList items={posts} titleLevel={3} className={listClass} />; }; const components: MDXComponents = { @@ -326,17 +324,20 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => { <> <Head> <title>{pageTitle}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={pageDescription} /> <meta property="og:url" content={website.url} /> <meta property="og:title" content={pageTitle} /> <meta property="og:description" content={pageDescription} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-homepage" type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-breadcrumb" type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx index faf76b1..9b0cc98 100644 --- a/src/pages/mentions-legales.tsx +++ b/src/pages/mentions-legales.tsx @@ -1,5 +1,5 @@ -import { MDXComponents } from 'mdx/types'; -import { GetStaticProps } from 'next'; +import type { MDXComponents } from 'mdx/types'; +import type { GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; @@ -7,11 +7,12 @@ import { getLayout, Link, PageLayout, - type PageLayoutProps, ResponsiveImage, + type MetaData, } from '../components'; import LegalNoticeContent, { meta } from '../content/pages/legal-notice.mdx'; -import { type NextPageWithLayout } from '../types'; +import type { NextPageWithLayout } from '../types'; +import { ROUTES } from '../utils/constants'; import { getSchemaJson, getSinglePageSchema, @@ -20,6 +21,11 @@ import { import { loadTranslation } from '../utils/helpers/server'; import { useBreadcrumb, useSettings } from '../utils/hooks'; +const components: MDXComponents = { + Image: ResponsiveImage, + Link, +}; + /** * Legal Notice page. */ @@ -27,10 +33,10 @@ const LegalNoticePage: NextPageWithLayout = () => { const { dates, intro, seo, title } = meta; const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/mentions-legales`, + url: ROUTES.LEGAL_NOTICE, }); - const headerMeta: PageLayoutProps['headerMeta'] = { + const headerMeta: MetaData = { publication: { date: dates.publication, }, @@ -41,11 +47,6 @@ const LegalNoticePage: NextPageWithLayout = () => { : undefined, }; - const components: MDXComponents = { - Image: ResponsiveImage, - Link, - }; - const { website } = useSettings(); const { asPath } = useRouter(); const webpageSchema = getWebPageSchema({ @@ -65,6 +66,10 @@ const LegalNoticePage: NextPageWithLayout = () => { title, }); const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]); + const page = { + title: `${seo.title} - ${website.name}`, + url: `${website.url}${asPath}`, + }; return ( <PageLayout @@ -76,14 +81,17 @@ const LegalNoticePage: NextPageWithLayout = () => { withToC={true} > <Head> - <title>{`${seo.title} - ${website.name}`}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> - <meta property="og:title" content={`${seo.title} - ${website.name}`} /> + <meta property="og:title" content={page.title} /> <meta property="og:description" content={intro} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-legal-notice" type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index ba03d9b..9981868 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -1,10 +1,11 @@ -import { MDXComponents } from 'mdx/types'; -import { GetStaticPaths, GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { MDXComponents } from 'mdx/types'; +import type { GetStaticPaths, GetStaticProps } from 'next'; import dynamic from 'next/dynamic'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import { ComponentType } from 'react'; +import type { ComponentType } from 'react'; import { useIntl } from 'react-intl'; import { Code, @@ -14,20 +15,17 @@ import { Overview, type OverviewMeta, PageLayout, - type PageLayoutProps, ResponsiveImage, type ResponsiveImageProps, Sharing, SocialLink, type SocialWebsite, Spinner, + type MetaData, } from '../../components'; import styles from '../../styles/pages/project.module.scss'; -import { - type NextPageWithLayout, - type ProjectPreview, - type Repos, -} from '../../types'; +import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types'; +import { ROUTES } from '../../utils/constants'; import { capitalize, getSchemaJson, @@ -40,15 +38,17 @@ import { loadTranslation, type Messages, } from '../../utils/helpers/server'; -import { - type RepoData, - useBreadcrumb, - useGithubApi, - useSettings, -} from '../../utils/hooks'; +import { useBreadcrumb, useGithubApi, useSettings } from '../../utils/hooks'; -const BorderedImage = (props: ResponsiveImageProps) => { - return <ResponsiveImage withBorders={true} {...props} />; +const BorderedImage = (props: ResponsiveImageProps) => ( + <ResponsiveImage withBorders={true} {...props} /> +); + +const components: MDXComponents = { + Code, + Gallery, + Image: BorderedImage, + Link, }; type ProjectPageProps = { @@ -65,28 +65,24 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => { const intl = useIntl(); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/projets/${id}`, + url: `${ROUTES.PROJECTS}/${id}`, }); const ProjectContent: ComponentType<MDXComponents> = dynamic( - () => import(`../../content/projects/${id}.mdx`), + async () => import(`../../content/projects/${id}.mdx`), { loading: () => <Spinner />, } ); - const components: MDXComponents = { - Code, - Gallery, - Image: BorderedImage, - Link, - }; - const { website } = useSettings(); const { asPath } = useRouter(); - const pageUrl = `${website.url}${asPath}`; + const page = { + title: `${seo.title} - ${website.name}`, + url: `${website.url}${asPath}`, + }; - const headerMeta: PageLayoutProps['headerMeta'] = { + const headerMeta: MetaData = { publication: { date: dates.publication }, update: dates.update && dates.update !== dates.publication @@ -97,7 +93,7 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => { /** * Retrieve the repositories links. * - * @param {Repos} repos - A repositories object. + * @param {Repos} repositories - A repositories object. * @returns {JSX.Element[]} - An array of SocialLink. */ const getReposLinks = (repositories: Repos): JSX.Element[] => { @@ -113,43 +109,45 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => { return links; }; - const { isError, isLoading, data } = useGithubApi(meta.repos!.github!); + const { isError, isLoading, data } = useGithubApi( + /* + * Repo should be defined for each project so for now it is safe for my + * use-case. However, I should refactored it to handle cases where it is + * not defined. The logic should be extracted in another component I think. + * + * TODO: fix this hardly readable argument + */ + meta.repos ? meta.repos.github ?? '' : '' + ); + + if (isError) return 'Error'; + if (isLoading || !data) return <Spinner />; - const getGithubData = (key: keyof RepoData) => { - if (isError) return 'Error'; - if (isLoading || !data) return <Spinner />; + 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`; - switch (key) { - case 'created_at': - return data.created_at; - case 'updated_at': - return data.updated_at; - case 'stargazers_count': - const stars = intl.formatMessage( - { - defaultMessage: - '{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}', - id: 'Gnf1Si', - description: 'Projets: Github stars count', - }, - { starsCount: data.stargazers_count } - ); - return ( - <> - ⭐ - <Link href={`https://github.com/${repos!.github}/stargazers`}> - {stars} - </Link> - </> - ); - } + return ( + <> + ⭐ + <Link href={popularityUrl}>{stars}</Link> + </> + ); }; const overviewData: OverviewMeta = { - creation: data && { date: getGithubData('created_at') as string }, - update: data && { date: getGithubData('updated_at') as string }, + creation: { date: data.created_at }, + update: { date: data.updated_at }, license, - popularity: data && getGithubData('stargazers_count'), + popularity: repos?.github && getRepoPopularity(repos.github), repositories: repos ? getReposLinks(repos) : undefined, technologies, }; @@ -176,16 +174,20 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => { return ( <> <Head> - <title>{`${seo.title} - ${website.name}`}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${pageUrl}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> <meta property="og:title" content={title} /> <meta property="og:description" content={intro} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-project" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout @@ -197,8 +199,9 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => { withToC={true} widgets={[ <Sharing + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="sharing-widget" - data={{ excerpt: intro, title, url: pageUrl }} + data={{ excerpt: intro, title, url: page.url }} media={[ 'diaspora', 'email', @@ -226,8 +229,7 @@ export const getStaticProps: GetStaticProps<ProjectPageProps> = async ({ params, }) => { const translation = await loadTranslation(locale); - const { slug } = params!; - const project = await getProjectData(slug as string); + const project = await getProjectData(params ? (params.slug as string) : ''); return { props: { @@ -237,7 +239,7 @@ export const getStaticProps: GetStaticProps<ProjectPageProps> = async ({ }; }; -export const getStaticPaths: GetStaticPaths = async () => { +export const getStaticPaths: GetStaticPaths = () => { const filenames = getProjectFilenames(); const paths = filenames.map((filename) => { return { diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx index a145401..97963dd 100644 --- a/src/pages/projets/index.tsx +++ b/src/pages/projets/index.tsx @@ -1,5 +1,5 @@ -import { MDXComponents } from 'mdx/types'; -import { GetStaticProps } from 'next'; +import type { MDXComponents } from 'mdx/types'; +import type { GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; @@ -12,12 +12,13 @@ import { } from '../../components'; import PageContent, { meta } from '../../content/pages/projects.mdx'; import styles from '../../styles/pages/projects.module.scss'; -import { type NextPageWithLayout, type ProjectCard } from '../../types'; +import type { NextPageWithLayout, ProjectCard } from '../../types'; +import { ROUTES } from '../../utils/constants'; import { getSchemaJson, getSinglePageSchema, getWebPageSchema, -} from '../../utils/helpers/'; +} from '../../utils/helpers'; import { getProjectsCard, loadTranslation, @@ -25,6 +26,10 @@ import { } from '../../utils/helpers/server'; import { useBreadcrumb, useSettings } from '../../utils/hooks'; +const components: MDXComponents = { + Link, +}; + type ProjectsPageProps = { projects: ProjectCard[]; translation?: Messages; @@ -37,7 +42,7 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => { const { dates, seo, title } = meta; const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/projets`, + url: ROUTES.PROJECTS, }); const items: CardsListItem[] = projects.map( @@ -47,18 +52,14 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => { return { cover, id: id as string, - meta: { technologies: technologies }, + meta: { technologies }, tagline, title: projectTitle, - url: `/projets/${slug}`, + url: `${ROUTES.PROJECTS}/${slug}`, }; } ); - const components: MDXComponents = { - Link, - }; - const { website } = useSettings(); const { asPath } = useRouter(); const webpageSchema = getWebPageSchema({ @@ -78,20 +79,28 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => { title, }); const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]); + const page = { + title: `${seo.title} - ${website.name}`, + url: `${website.url}${asPath}`, + }; return ( <> <Head> - <title>{`${seo.title} - ${website.name}`}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> - <meta property="og:title" content={`${seo.title} - ${website.name}`} /> + <meta property="og:title" content={page.title} /> <meta property="og:description" content={seo.description} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-projects" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx index 26cfd91..971d04a 100644 --- a/src/pages/recherche/index.tsx +++ b/src/pages/recherche/index.tsx @@ -1,7 +1,9 @@ -import { GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; +import { useCallback } from 'react'; import { useIntl } from 'react-intl'; import { getLayout, @@ -19,12 +21,13 @@ import { getTotalThematics, getTotalTopics, } from '../../services/graphql'; -import { - type NextPageWithLayout, - type RawArticle, - type RawThematicPreview, - type RawTopicPreview, +import type { + NextPageWithLayout, + RawArticle, + RawThematicPreview, + RawTopicPreview, } from '../../types'; +import { ROUTES } from '../../utils/constants'; import { getBlogSchema, getLinksListItems, @@ -72,11 +75,14 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ }); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/recherche`, + url: ROUTES.SEARCH, }); const { blog, website } = useSettings(); - const pageTitle = `${title} - ${website.name}`; + const page = { + title: `${title} - ${website.name}`, + url: `${website.url}${asPath}`, + }; const pageDescription = query.s ? intl.formatMessage( { @@ -99,7 +105,7 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ description: pageDescription, locale: website.locales.default, slug: asPath, - title: pageTitle, + title: page.title, }); const blogSchema = getBlogSchema({ isSinglePage: false, @@ -122,16 +128,16 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ search: query.s as string, }); - const totalArticles = useDataFromAPI<number>(() => + const totalArticles = useDataFromAPI<number>(async () => getTotalArticles(query.s as string) ); /** * Load more posts handler. */ - const loadMore = () => { + const loadMore = useCallback(() => { setSize((prevSize) => prevSize + 1); - }; + }, [setSize]); const thematicsListTitle = intl.formatMessage({ defaultMessage: 'Thematics', @@ -144,20 +150,25 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ description: 'SearchPage: topics list widget title', id: 'N804XO', }); + const postsListBaseUrl = `${ROUTES.SEARCH}/page/`; return ( <> <Head> - <title>{pageTitle}</title> + <title>{page.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={pageDescription} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={page.url} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="website" /> <meta property="og:title" content={title} /> <meta property="og:description" content={pageDescription} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-blog" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout @@ -167,6 +178,7 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ headerMeta={{ total: totalArticles }} widgets={[ <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="thematics-list" items={getLinksListItems( thematicsList.map((thematic) => @@ -177,6 +189,7 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ level={2} />, <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="topics-list" items={getLinksListItems( topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic')) @@ -188,20 +201,21 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ > {data && data.length > 0 ? ( <PostsList - baseUrl="/recherche/page/" + baseUrl={postsListBaseUrl} byYear={true} - isLoading={isLoadingMore || isLoadingInitialData} + isLoading={isLoadingMore ?? isLoadingInitialData} loadMore={loadMore} posts={getPostsList(data)} - searchPage="/recherche/" + searchPage={ROUTES.SEARCH} showLoadMoreBtn={hasNextPage} - total={totalArticles || 0} + total={totalArticles ?? 0} /> ) : ( <Spinner /> )} - {error && ( + {error ? ( <Notice + // eslint-disable-next-line react/jsx-no-literals -- Kind allowed kind="error" message={intl.formatMessage({ defaultMessage: 'Failed to load.', @@ -209,7 +223,7 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ id: 'fOe8rH', })} /> - )} + ) : null} </PageLayout> </> ); diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx index 6308978..94541b9 100644 --- a/src/pages/sujet/[slug].tsx +++ b/src/pages/sujet/[slug].tsx @@ -1,17 +1,18 @@ -import { GetStaticPaths, GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { ParsedUrlQuery } from 'querystring'; +import type { GetStaticPaths, GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import { ParsedUrlQuery } from 'querystring'; import { useIntl } from 'react-intl'; import { getLayout, Heading, LinksListWidget, PageLayout, - type PageLayoutProps, PostsList, ResponsiveImage, + type MetaData, } from '../../components'; import { getAllTopicsSlugs, @@ -20,11 +21,8 @@ import { getTotalTopics, } from '../../services/graphql'; import styles from '../../styles/pages/topic.module.scss'; -import { - type NextPageWithLayout, - type PageLink, - type Topic, -} from '../../types'; +import type { NextPageWithLayout, PageLink, Topic } from '../../types'; +import { ROUTES } from '../../utils/constants'; import { getLinksListItems, getPageLinkFromRawData, @@ -58,10 +56,10 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ const intl = useIntl(); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/sujet/${slug}`, + url: `${ROUTES.TOPICS}/${slug}`, }); - const headerMeta: PageLayoutProps['headerMeta'] = { + const headerMeta: MetaData = { publication: { date: dates.publication }, update: dates.update ? { date: dates.update } : undefined, website: officialWebsite, @@ -101,28 +99,32 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ id: '/sRqPT', }); - const getPageHeading = () => { - return ( - <> - {cover && <ResponsiveImage className={styles.logo} {...cover} />} - {title} - </> - ); - }; + const getPageHeading = () => ( + <> + {cover ? <ResponsiveImage className={styles.logo} {...cover} /> : null} + {title} + </> + ); + const pageUrl = `${website.url}${asPath}`; + const postsListBaseUrl = `${ROUTES.TOPICS}/page/`; return ( <> <Head> <title>{seo.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={pageUrl} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> <meta property="og:title" content={title} /> <meta property="og:description" content={intro} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-project" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout @@ -135,12 +137,14 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ thematics ? [ <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="related-thematics" items={getLinksListItems(thematics)} title={thematicsListTitle} level={2} />, <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="topics" items={getLinksListItems(topics)} title={topicsListTitle} @@ -150,8 +154,9 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ : [] } > - {content && <div dangerouslySetInnerHTML={{ __html: content }} />} - {articles && ( + {/*eslint-disable-next-line react/no-danger -- Necessary for content*/} + {content ? <div dangerouslySetInnerHTML={{ __html: content }} /> : null} + {articles ? ( <> <Heading level={2}> {intl.formatMessage( @@ -164,15 +169,15 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ )} </Heading> <PostsList - baseUrl="/sujet/page/" + baseUrl={postsListBaseUrl} byYear={true} posts={getPostsWithUrl(articles)} - searchPage="/recherche/" + searchPage={ROUTES.SEARCH} titleLevel={3} total={articles.length} /> </> - )} + ) : null} </PageLayout> </> ); @@ -181,17 +186,15 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ TopicPage.getLayout = (page) => getLayout(page, { useGrid: true, withExtraPadding: true }); -interface TopicParams extends ParsedUrlQuery { +type TopicParams = { slug: string; -} +} & ParsedUrlQuery; export const getStaticProps: GetStaticProps<TopicPageProps> = async ({ locale, params, }) => { - const currentTopic = await getTopicBySlug( - params!.slug as TopicParams['slug'] - ); + const currentTopic = await getTopicBySlug((params as TopicParams).slug); const totalTopics = await getTotalTopics(); const allTopicsEdges = await getTopicsPreview({ first: totalTopics, @@ -200,7 +203,7 @@ export const getStaticProps: GetStaticProps<TopicPageProps> = async ({ getPageLinkFromRawData(edge.node, 'topic') ); const topicsLinks = allTopics.filter( - (topic) => topic.url !== `/sujet/${params!.slug as TopicParams['slug']}` + (topic) => topic.url !== `${ROUTES.TOPICS}/${(params as TopicParams).slug}` ); const translation = await loadTranslation(locale); diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx index 7712fff..cf610c7 100644 --- a/src/pages/thematique/[slug].tsx +++ b/src/pages/thematique/[slug].tsx @@ -1,16 +1,17 @@ -import { GetStaticPaths, GetStaticProps } from 'next'; +/* eslint-disable max-statements */ +import type { ParsedUrlQuery } from 'querystring'; +import type { GetStaticPaths, GetStaticProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import { ParsedUrlQuery } from 'querystring'; import { useIntl } from 'react-intl'; import { getLayout, Heading, LinksListWidget, PageLayout, - type PageLayoutProps, PostsList, + type MetaData, } from '../../components'; import { getAllThematicsSlugs, @@ -18,11 +19,8 @@ import { getThematicsPreview, getTotalThematics, } from '../../services/graphql'; -import { - type NextPageWithLayout, - type PageLink, - type Thematic, -} from '../../types'; +import type { NextPageWithLayout, PageLink, Thematic } from '../../types'; +import { ROUTES } from '../../utils/constants'; import { getLinksListItems, getPageLinkFromRawData, @@ -49,10 +47,10 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ const intl = useIntl(); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, - url: `/thematique/${slug}`, + url: `${ROUTES.THEMATICS.INDEX}/${slug}`, }); - const headerMeta: PageLayoutProps['headerMeta'] = { + const headerMeta: MetaData = { publication: { date: dates.publication }, update: dates.update ? { date: dates.update } : undefined, total: articles ? articles.length : undefined, @@ -89,20 +87,26 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ description: 'ThematicPage: related topics list widget title', id: '/42Z0z', }); + const pageUrl = `${website.url}${asPath}`; + const postsListBaseUrl = `${ROUTES.THEMATICS.INDEX}/page/`; return ( <> <Head> <title>{seo.title}</title> + {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} <meta name="description" content={seo.description} /> - <meta property="og:url" content={`${website.url}${asPath}`} /> + <meta property="og:url" content={pageUrl} /> + {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */} <meta property="og:type" content="article" /> <meta property="og:title" content={title} /> <meta property="og:description" content={intro} /> </Head> <Script + // eslint-disable-next-line react/jsx-no-literals -- Id allowed id="schema-project" type="application/ld+json" + // eslint-disable-next-line react/no-danger -- Necessary for schema dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} /> <PageLayout @@ -115,12 +119,14 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ topics ? [ <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="thematics" items={getLinksListItems(thematics)} title={thematicsListTitle} level={2} />, <LinksListWidget + // eslint-disable-next-line react/jsx-no-literals -- Key allowed key="related-topics" items={getLinksListItems(topics)} title={topicsListTitle} @@ -130,8 +136,9 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ : [] } > + {/*eslint-disable-next-line react/no-danger -- Necessary for content*/} <div dangerouslySetInnerHTML={{ __html: content }} /> - {articles && ( + {articles ? ( <> <Heading level={2}> {intl.formatMessage( @@ -144,15 +151,15 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ )} </Heading> <PostsList - baseUrl="/thematique/page/" + baseUrl={postsListBaseUrl} byYear={true} posts={getPostsWithUrl(articles)} - searchPage="/recherche/" + searchPage={ROUTES.SEARCH} titleLevel={3} total={articles.length} /> </> - )} + ) : null} </PageLayout> </> ); @@ -161,16 +168,16 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ ThematicPage.getLayout = (page) => getLayout(page, { useGrid: true, withExtraPadding: true }); -interface ThematicParams extends ParsedUrlQuery { +type ThematicParams = { slug: string; -} +} & ParsedUrlQuery; export const getStaticProps: GetStaticProps<ThematicPageProps> = async ({ locale, params, }) => { const currentThematic = await getThematicBySlug( - params!.slug as ThematicParams['slug'] + (params as ThematicParams).slug ); const totalThematics = await getTotalThematics(); const allThematicsEdges = await getThematicsPreview({ @@ -181,7 +188,8 @@ export const getStaticProps: GetStaticProps<ThematicPageProps> = async ({ ); const allThematicsLinks = allThematics.filter( (thematic) => - thematic.url !== `/thematique/${params!.slug as ThematicParams['slug']}` + thematic.url !== + `${ROUTES.THEMATICS.INDEX}/${(params as ThematicParams).slug}` ); const translation = await loadTranslation(locale); diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..e642af9 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,32 @@ +export const PERSONAL_LINKS = { + GITHUB: 'https://github.com/ArmandPhilippot', + GITLAB: 'https://gitlab.com/ArmandPhilippot', + LINKEDIN: 'https://www.linkedin.com/in/armandphilippot', + SHAARLI: 'https://shaarli.armandphilippot.com/', +} as const; + +/** + * App routes. + * + * All static routes should be configured here to avoid 404 if a route changes. + */ +export const ROUTES = { + ARTICLE: '/article', + BLOG: '/blog', + CONTACT: '/contact', + CV: '/cv', + LEGAL_NOTICE: '/mentions-legales', + NOT_FOUND: '/404', + PROJECTS: '/projets', + RSS: '/feed', + SEARCH: '/recherche', + THEMATICS: { + INDEX: '/thematique', + FREE: '/thematique/libre', + LINUX: '/thematique/linux', + WEB_DEV: '/thematique/developpement-web', + }, + TOPICS: '/sujet', +} as const; + +// cSpell:ignore legales thematique developpement diff --git a/src/utils/helpers/pages.ts b/src/utils/helpers/pages.ts index 6b27b6d..84854cd 100644 --- a/src/utils/helpers/pages.ts +++ b/src/utils/helpers/pages.ts @@ -1,13 +1,14 @@ -import { type LinksListItems, type Post } from '../../components'; +import type { LinksListItems, Post } from '../../components'; import { getArticleFromRawData } from '../../services/graphql'; -import { - type Article, - type EdgesResponse, - type PageLink, - type RawArticle, - type RawThematicPreview, - type RawTopicPreview, +import type { + Article, + EdgesResponse, + PageLink, + RawArticle, + RawThematicPreview, + RawTopicPreview, } from '../../types'; +import { ROUTES } from '../constants'; import { getImageFromRawData } from './images'; /** @@ -25,11 +26,13 @@ export const getPageLinkFromRawData = ( kind: 'thematic' | 'topic' ): PageLink => { const { databaseId, featuredImage, slug, title } = data; - const baseUrl = kind === 'thematic' ? '/thematique/' : '/sujet/'; + const baseUrl = `${ + kind === 'thematic' ? ROUTES.THEMATICS.INDEX : ROUTES.TOPICS + }/`; return { id: databaseId, - logo: featuredImage ? getImageFromRawData(featuredImage?.node) : undefined, + logo: featuredImage ? getImageFromRawData(featuredImage.node) : undefined, name: title, url: `${baseUrl}${slug}`, }; @@ -57,14 +60,13 @@ export const sortPageLinksByName = (a: PageLink, b: PageLink) => { * @param {PageLink[]} links - An array of page links. * @returns {LinksListItem[]} An array of links items. */ -export const getLinksListItems = (links: PageLink[]): LinksListItems[] => { - return links.map((link) => { +export const getLinksListItems = (links: PageLink[]): LinksListItems[] => + links.map((link) => { return { name: link.name, url: link.url, }; }); -}; /** * Retrieve the posts list with the article URL. @@ -72,14 +74,13 @@ export const getLinksListItems = (links: PageLink[]): LinksListItems[] => { * @param {Article[]} posts - An array of articles. * @returns {Post[]} An array of posts with full article URL. */ -export const getPostsWithUrl = (posts: Article[]): Post[] => { - return posts.map((post) => { +export const getPostsWithUrl = (posts: Article[]): Post[] => + posts.map((post) => { return { ...post, url: `/article/${post.slug}`, }; }); -}; /** * Retrieve the posts list from raw data. @@ -89,11 +90,11 @@ export const getPostsWithUrl = (posts: Article[]): Post[] => { */ export const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => { const articlesList: RawArticle[] = []; - rawData.forEach((articleData) => + rawData.forEach((articleData) => { articleData.edges.forEach((edge) => { articlesList.push(edge.node); - }) - ); + }); + }); return getPostsWithUrl( articlesList.map((article) => getArticleFromRawData(article)) diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts index 28f3c7b..0381c68 100644 --- a/src/utils/helpers/rss.ts +++ b/src/utils/helpers/rss.ts @@ -4,8 +4,9 @@ import { getArticles, getTotalArticles, } from '../../services/graphql'; -import { type Article } from '../../types'; -import { settings } from '../../utils/config'; +import type { Article } from '../../types'; +import { settings } from '../config'; +import { ROUTES } from '../constants'; /** * Retrieve the data for all the articles. @@ -17,9 +18,9 @@ const getAllArticles = async (): Promise<Article[]> => { const rawArticles = await getArticles({ first: totalArticles }); const articles: Article[] = []; - rawArticles.edges.forEach((edge) => - articles.push(getArticleFromRawData(edge.node)) - ); + rawArticles.edges.forEach((edge) => { + articles.push(getArticleFromRawData(edge.node)); + }); return articles; }; @@ -43,8 +44,8 @@ export const generateFeed = async (): Promise<Feed> => { copyright, description: process.env.APP_FEED_DESCRIPTION, feedLinks: { - json: `${settings.url}/feed/json`, - atom: `${settings.url}/feed/atom`, + json: `${settings.url}${ROUTES.RSS}/json`, + atom: `${settings.url}${ROUTES.RSS}/atom`, }, generator: 'Feed & NextJS', id: settings.url, @@ -58,10 +59,10 @@ export const generateFeed = async (): Promise<Feed> => { articles.forEach((article) => { feed.addItem({ content: article.intro, - date: new Date(article.meta!.dates.publication), + date: new Date(article.meta.dates.publication), description: article.intro, id: `${article.id}`, - link: `${settings.url}/article/${article.slug}`, + link: `${settings.url}${ROUTES.ARTICLE}/${article.slug}`, title: article.title, }); }); diff --git a/src/utils/helpers/schema-org.ts b/src/utils/helpers/schema-org.ts index 82f99c2..12bad28 100644 --- a/src/utils/helpers/schema-org.ts +++ b/src/utils/helpers/schema-org.ts @@ -1,4 +1,4 @@ -import { +import type { AboutPage, Article, Blog, @@ -7,8 +7,9 @@ import { Graph, WebPage, } from 'schema-dts'; -import { type Dates } from '../../types'; -import { settings } from '../../utils/config'; +import type { Dates } from '../../types'; +import { settings } from '../config'; +import { ROUTES } from '../constants'; export type GetBlogSchemaProps = { /** @@ -146,7 +147,7 @@ export const getSinglePageSchema = <T extends SinglePageSchemaKind>({ copyrightYear: publicationDate.getFullYear(), creator: { '@id': `${settings.url}/#branding` }, dateCreated: publicationDate.toISOString(), - dateModified: updateDate && updateDate.toISOString(), + dateModified: updateDate?.toISOString(), datePublished: publicationDate.toISOString(), editor: { '@id': `${settings.url}/#branding` }, headline: title, @@ -157,7 +158,7 @@ export const getSinglePageSchema = <T extends SinglePageSchemaKind>({ isPartOf: kind === 'post' ? { - '@id': `${settings.url}/blog`, + '@id': `${settings.url}${ROUTES.BLOG}`, } : undefined, mainEntityOfPage: { '@id': `${settings.url}${slug}` }, @@ -206,7 +207,7 @@ export const getWebPageSchema = ({ breadcrumb: { '@id': `${settings.url}/#breadcrumb` }, lastReviewed: updateDate, name: title, - description: description, + description, inLanguage: locale, reviewedBy: { '@id': `${settings.url}/#branding` }, url: `${settings.url}${slug}`, diff --git a/src/utils/hooks/use-breadcrumb.tsx b/src/utils/hooks/use-breadcrumb.ts index f4506d7..5839299 100644 --- a/src/utils/hooks/use-breadcrumb.tsx +++ b/src/utils/hooks/use-breadcrumb.ts @@ -1,6 +1,8 @@ +/* eslint-disable max-statements */ import { useIntl } from 'react-intl'; -import { BreadcrumbList } from 'schema-dts'; -import { BreadcrumbItem } from '../../components'; +import type { BreadcrumbList } from 'schema-dts'; +import type { BreadcrumbItem } from '../../components'; +import { ROUTES } from '../constants'; import { slugify } from '../helpers'; import { useSettings } from './use-settings'; @@ -38,13 +40,13 @@ export const useBreadcrumb = ({ }: useBreadcrumbProps): useBreadcrumbReturn => { const intl = useIntl(); const { website } = useSettings(); - const isArticle = url.startsWith('/article/'); + const isArticle = url.startsWith(`${ROUTES.ARTICLE}/`); const isHome = url === '/'; const isPageNumber = url.includes('/page/'); - const isProject = url.startsWith('/projets/'); - const isSearch = url.startsWith('/recherche'); - const isThematic = url.startsWith('/thematique/'); - const isTopic = url.startsWith('/sujet/'); + const isProject = url.startsWith(`${ROUTES.PROJECTS}/`); + const isSearch = url.startsWith(ROUTES.SEARCH); + const isThematic = url.startsWith(`${ROUTES.THEMATICS.INDEX}/`); + const isTopic = url.startsWith(`${ROUTES.TOPICS}/`); const homeLabel = intl.formatMessage({ defaultMessage: 'Home', @@ -69,12 +71,12 @@ export const useBreadcrumb = ({ description: 'Breadcrumb: blog label', id: 'Es52wh', }); - items.push({ id: 'blog', name: blogLabel, url: '/blog' }); + items.push({ id: 'blog', name: blogLabel, url: ROUTES.BLOG }); schema.push({ '@type': 'ListItem', position: 2, name: blogLabel, - item: `${website.url}/blog`, + item: `${website.url}${ROUTES.BLOG}`, }); } @@ -84,12 +86,12 @@ export const useBreadcrumb = ({ description: 'Breadcrumb: projects label', id: '28GZdv', }); - items.push({ id: 'blog', name: projectsLabel, url: '/projets' }); + items.push({ id: 'projects', name: projectsLabel, url: ROUTES.PROJECTS }); schema.push({ '@type': 'ListItem', position: 2, name: projectsLabel, - item: `${website.url}/projets`, + item: `${website.url}${ROUTES.PROJECTS}`, }); } |
