aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-26 15:54:28 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:23:48 +0200
commit70efcfeaa0603415dd992cb662d8efb960e6e49a (patch)
tree5d37e98fae9aa7e5c3d8ef30a10db9fed9b63e36 /src
parent31695306bfed44409f03006ea717fd2cceff8f87 (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.tsx60
-rw-r--r--src/i18n/en.json12
-rw-r--r--src/i18n/fr.json12
-rw-r--r--src/pages/404.tsx22
-rw-r--r--src/pages/article/[slug].tsx65
-rw-r--r--src/pages/blog/index.tsx71
-rw-r--r--src/pages/blog/page/[number].tsx51
-rw-r--r--src/pages/contact.tsx102
-rw-r--r--src/pages/cv.tsx133
-rw-r--r--src/pages/index.tsx129
-rw-r--r--src/pages/mentions-legales.tsx36
-rw-r--r--src/pages/projets/[slug].tsx134
-rw-r--r--src/pages/projets/index.tsx37
-rw-r--r--src/pages/recherche/index.tsx54
-rw-r--r--src/pages/sujet/[slug].tsx63
-rw-r--r--src/pages/thematique/[slug].tsx46
-rw-r--r--src/utils/constants.ts32
-rw-r--r--src/utils/helpers/pages.ts39
-rw-r--r--src/utils/helpers/rss.ts19
-rw-r--r--src/utils/helpers/schema-org.ts13
-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 (
- <>
- ⭐&nbsp;
- <Link href={`https://github.com/${repos!.github}/stargazers`}>
- {stars}
- </Link>
- </>
- );
- }
+ return (
+ <>
+ ⭐&nbsp;
+ <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}`,
});
}