aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-12-01 13:26:44 +0100
committerArmand Philippot <git@armandphilippot.com>2023-12-01 17:23:19 +0100
commitdfdbf6cac1fe3719dc71e130129d28e04ba4e225 (patch)
treef865bdad53cef95bdfb10fc04174a0173ab36f15
parent5b762b1b669454a89899c4bdf6008027d9615acf (diff)
refactor(pages): refine Thematic pages
* add a table of contents (however posts heading are not included) * rename posts list section title * add a useThematic hook to refresh thematic contents * add a useThematicLists hook to refresh thematics list * add a `notIn` filter in thematics list fetcher to directly remove unwanted thematics * add Cypress tests
-rw-r--r--src/i18n/en.json12
-rw-r--r--src/i18n/fr.json14
-rw-r--r--src/pages/sujet/[slug].tsx2
-rw-r--r--src/pages/thematique/[slug].tsx181
-rw-r--r--src/services/graphql/fetchers/thematics/fetch-thematics-list.ts4
-rw-r--r--src/services/graphql/fetchers/topics/fetch-topics-list.ts4
-rw-r--r--src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts1
-rw-r--r--src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts40
-rw-r--r--src/services/graphql/helpers/convert-wp-topic-to-topic.test.ts1
-rw-r--r--src/services/graphql/helpers/convert-wp-topic-to-topic.ts41
-rw-r--r--src/styles/abstracts/placeholders/_links.scss2
-rw-r--r--src/styles/pages/blog.module.scss4
-rw-r--r--src/types/data.ts2
-rw-r--r--src/types/gql.ts1
-rw-r--r--src/utils/hooks/index.ts2
-rw-r--r--src/utils/hooks/use-thematic/index.ts1
-rw-r--r--src/utils/hooks/use-thematic/use-thematic.test.ts56
-rw-r--r--src/utils/hooks/use-thematic/use-thematic.ts31
-rw-r--r--src/utils/hooks/use-thematics-list/index.ts1
-rw-r--r--src/utils/hooks/use-thematics-list/use-thematics-list.test.ts48
-rw-r--r--src/utils/hooks/use-thematics-list/use-thematics-list.ts50
-rw-r--r--tests/cypress/e2e/pages/thematic.cy.ts41
-rw-r--r--tests/msw/handlers/thematics/thematics-list.handler.ts11
-rw-r--r--tests/msw/handlers/topics/topics-list.handler.ts9
-rw-r--r--tests/msw/schema/types/thematic.types.ts1
-rw-r--r--tests/msw/schema/types/topic.types.ts1
26 files changed, 446 insertions, 115 deletions
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 671e2b1..c449756 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -299,10 +299,6 @@
"defaultMessage": "Page not found",
"description": "Error404Page: page title"
},
- "LszkU6": {
- "defaultMessage": "All posts in {thematicName}",
- "description": "ThematicPage: posts list heading"
- },
"MJLr6U": {
"defaultMessage": "CV",
"description": "SiteNavbar: main nav - cv link"
@@ -555,6 +551,10 @@
"defaultMessage": "Reading time:",
"description": "PageHeader: reading time label"
},
+ "jrRBeb": {
+ "defaultMessage": "Browse posts in {thematicName} thematic",
+ "description": "ThematicPage: posts list heading"
+ },
"kq+fzI": {
"defaultMessage": "Cover of {pageTitle}",
"description": "RecentPosts: card cover accessible name"
@@ -627,6 +627,10 @@
"defaultMessage": "Discover search results for {query} on {websiteName}.",
"description": "SearchPage: SEO - Meta description"
},
+ "rVoW4G": {
+ "defaultMessage": "Thematics are loading...",
+ "description": "ThematicPage: loading thematics message"
+ },
"s57FTB": {
"defaultMessage": "Share",
"description": "Article: sharing widget title"
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index c8b4058..9e5754a 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -16,7 +16,7 @@
"description": "BlogPage: SEO - Page title"
},
"/42Z0z": {
- "defaultMessage": "Sujets liés",
+ "defaultMessage": "Sujets connexes",
"description": "ThematicPage: related topics list widget title"
},
"/EfcyW": {
@@ -299,10 +299,6 @@
"defaultMessage": "Page non trouvée",
"description": "Error404Page: page title"
},
- "LszkU6": {
- "defaultMessage": "Tous les articles dans {thematicName}",
- "description": "ThematicPage: posts list heading"
- },
"MJLr6U": {
"defaultMessage": "CV",
"description": "SiteNavbar: main nav - cv link"
@@ -555,6 +551,10 @@
"defaultMessage": "Temps de lecture :",
"description": "PageHeader: reading time label"
},
+ "jrRBeb": {
+ "defaultMessage": "Parcourir les articles de la thématique {thematicName}",
+ "description": "ThematicPage: posts list heading"
+ },
"kq+fzI": {
"defaultMessage": "Illustration de {pageTitle}",
"description": "RecentPosts: card cover accessible name"
@@ -627,6 +627,10 @@
"defaultMessage": "Découvrez les résultats de recherche pour {query} sur {websiteName}.",
"description": "SearchPage: SEO - Meta description"
},
+ "rVoW4G": {
+ "defaultMessage": "Les thématiques sont en cours de chargement…",
+ "description": "ThematicPage: loading thematics message"
+ },
"s57FTB": {
"defaultMessage": "Partager",
"description": "Article: sharing widget title"
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 483df48..185756b 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -97,7 +97,7 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
const getPageHeading = () => (
<>
- {cover ? <NextImage {...cover} className={styles.logo} /> : null}
+ {cover ? <NextImage {...cover} className={styles['topic-logo']} /> : null}
{title}
</>
);
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index 487b18b..e290782 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -14,17 +14,24 @@ import {
PageHeader,
PageSidebar,
PageBody,
+ LoadingPage,
+ TocWidget,
+ Spinner,
} from '../../components';
import {
convertWPThematicPreviewToPageLink,
- convertWPThematicToThematic,
fetchAllThematicsSlugs,
fetchThematic,
fetchThematicsCount,
fetchThematicsList,
} from '../../services/graphql';
import styles from '../../styles/pages/blog.module.scss';
-import type { NextPageWithLayout, PageLink, Thematic } from '../../types';
+import type {
+ GraphQLConnection,
+ NextPageWithLayout,
+ WPThematic,
+ WPThematicPreview,
+} from '../../types';
import { CONFIG } from '../../utils/config';
import { ROUTES } from '../../utils/constants';
import {
@@ -33,29 +40,47 @@ import {
getSchemaJson,
getSinglePageSchema,
getWebPageSchema,
+ slugify,
} from '../../utils/helpers';
import { loadTranslation, type Messages } from '../../utils/helpers/server';
-import { useBreadcrumb } from '../../utils/hooks';
+import {
+ useBreadcrumb,
+ useHeadingsTree,
+ useThematic,
+ useThematicsList,
+} from '../../utils/hooks';
export type ThematicPageProps = {
- currentThematic: Thematic;
- thematics: PageLink[];
+ data: {
+ currentThematic: WPThematic;
+ otherThematics: GraphQLConnection<WPThematicPreview>;
+ totalThematics: number;
+ };
translation: Messages;
};
-const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
- currentThematic,
- thematics,
-}) => {
- const { content, intro, meta, slug, title } = currentThematic;
- const { articles, dates, seo, relatedTopics } = meta;
+const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ data }) => {
const intl = useIntl();
+ const { isFallback } = useRouter();
+ const { isLoading, thematic } = useThematic(
+ data.currentThematic.slug,
+ data.currentThematic
+ );
+ const { isLoading: areThematicsLoading, thematics } = useThematicsList({
+ fallback: data.otherThematics,
+ input: { first: data.totalThematics, where: { notIn: [thematic.id] } },
+ });
const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
- title,
- url: `${ROUTES.THEMATICS}/${slug}`,
+ title: data.currentThematic.title,
+ url: `${ROUTES.THEMATICS}/${data.currentThematic.slug}`,
});
+ const { ref, tree } = useHeadingsTree({ fromLevel: 2 });
+
+ if (isFallback || isLoading) return <LoadingPage />;
+
+ const { content, intro, meta, slug, title } = thematic;
+ const { articles, dates, seo, relatedTopics } = meta;
- const { asPath } = useRouter();
const webpageSchema = getWebPageSchema({
description: seo.description,
locale: CONFIG.locales.defaultLocale,
@@ -74,18 +99,41 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
});
const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
- const thematicsListTitle = intl.formatMessage({
- defaultMessage: 'Other thematics',
- description: 'ThematicPage: other thematics list widget title',
- id: 'KVSWGP',
- });
+ const messages = {
+ widgets: {
+ loadingThematicsList: intl.formatMessage({
+ defaultMessage: 'Thematics are loading...',
+ description: 'ThematicPage: loading thematics message',
+ id: 'rVoW4G',
+ }),
+ thematicsListTitle: intl.formatMessage({
+ defaultMessage: 'Other thematics',
+ description: 'ThematicPage: other thematics list widget title',
+ id: 'KVSWGP',
+ }),
+ tocTitle: intl.formatMessage({
+ defaultMessage: 'Table of Contents',
+ description: 'PageLayout: table of contents title',
+ id: 'eys2uX',
+ }),
+ topicsListTitle: intl.formatMessage({
+ defaultMessage: 'Related topics',
+ description: 'ThematicPage: related topics list widget title',
+ id: '/42Z0z',
+ }),
+ },
+ browsePostsTitle: intl.formatMessage(
+ {
+ defaultMessage: 'Browse posts in {thematicName} thematic',
+ description: 'ThematicPage: posts list heading',
+ id: 'jrRBeb',
+ },
+ { thematicName: title }
+ ),
+ };
- const topicsListTitle = intl.formatMessage({
- defaultMessage: 'Related topics',
- description: 'ThematicPage: related topics list widget title',
- id: '/42Z0z',
- });
- const pageUrl = `${CONFIG.url}${asPath}`;
+ const pageUrl = `${CONFIG.url}${slug}`;
+ const browsePostHeadingId = slugify(messages.browsePostsTitle);
return (
<Page breadcrumbs={breadcrumbItems}>
@@ -121,23 +169,33 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
updateDate: dates.update,
}}
/>
- <PageBody className={styles.body}>
- {/*eslint-disable-next-line react/no-danger -- Necessary for content*/}
- <div dangerouslySetInnerHTML={{ __html: content }} />
+ <PageSidebar>
+ <TocWidget
+ heading={<Heading level={2}>{messages.widgets.tocTitle}</Heading>}
+ tree={[
+ ...tree,
+ {
+ children: [],
+ depth: 2,
+ id: browsePostHeadingId,
+ label: messages.browsePostsTitle,
+ },
+ ]}
+ />
+ </PageSidebar>
+ <PageBody>
+ <div
+ className={styles.body}
+ // eslint-disable-next-line react/no-danger -- Necessary for content
+ dangerouslySetInnerHTML={{ __html: content }}
+ ref={ref}
+ />
{articles ? (
<>
- <Heading level={2}>
- {intl.formatMessage(
- {
- defaultMessage: 'All posts in {thematicName}',
- description: 'ThematicPage: posts list heading',
- id: 'LszkU6',
- },
- { thematicName: title }
- )}
+ <Heading id={browsePostHeadingId} level={2}>
+ {messages.browsePostsTitle}
</Heading>
<PostsList
- className={styles.list}
posts={getPostsWithUrl(articles)}
headingLvl={3}
sortByYear
@@ -146,20 +204,24 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
) : null}
</PageBody>
<PageSidebar>
- <LinksWidget
- heading={
- <Heading isFake level={3}>
- {thematicsListTitle}
- </Heading>
- }
- items={getLinksItemData(thematics)}
- />
+ {areThematicsLoading ? (
+ <Spinner>{messages.widgets.loadingThematicsList}</Spinner>
+ ) : (
+ <LinksWidget
+ heading={
+ <Heading level={2}>{messages.widgets.thematicsListTitle}</Heading>
+ }
+ items={getLinksItemData(
+ thematics.edges.map((edge) =>
+ convertWPThematicPreviewToPageLink(edge.node)
+ )
+ )}
+ />
+ )}
{relatedTopics ? (
<LinksWidget
heading={
- <Heading isFake level={3}>
- {topicsListTitle}
- </Heading>
+ <Heading level={2}>{messages.widgets.topicsListTitle}</Heading>
}
items={getLinksItemData(relatedTopics)}
/>
@@ -179,26 +241,21 @@ export const getStaticProps: GetStaticProps<ThematicPageProps> = async ({
locale,
params,
}) => {
- const currentThematic = await fetchThematic((params as ThematicParams).slug);
+ const thematic = await fetchThematic((params as ThematicParams).slug);
const totalThematics = await fetchThematicsCount();
- const allThematicsEdges = await fetchThematicsList({
+ const otherThematics = await fetchThematicsList({
first: totalThematics,
+ where: { notIn: [thematic.databaseId] },
});
- const allThematics = allThematicsEdges.edges.map((edge) =>
- convertWPThematicPreviewToPageLink(edge.node)
- );
- const allThematicsLinks = allThematics.filter(
- (thematic) =>
- thematic.url !== `${ROUTES.THEMATICS}/${(params as ThematicParams).slug}`
- );
const translation = await loadTranslation(locale);
return {
props: {
- currentThematic: JSON.parse(
- JSON.stringify(convertWPThematicToThematic(currentThematic))
- ),
- thematics: JSON.parse(JSON.stringify(allThematicsLinks)),
+ data: {
+ currentThematic: JSON.parse(JSON.stringify(thematic)),
+ otherThematics: JSON.parse(JSON.stringify(otherThematics)),
+ totalThematics,
+ },
translation,
},
};
diff --git a/src/services/graphql/fetchers/thematics/fetch-thematics-list.ts b/src/services/graphql/fetchers/thematics/fetch-thematics-list.ts
index 7e1e582..5dff5e1 100644
--- a/src/services/graphql/fetchers/thematics/fetch-thematics-list.ts
+++ b/src/services/graphql/fetchers/thematics/fetch-thematics-list.ts
@@ -12,13 +12,13 @@ export type ThematicsListResponse = {
thematics: Nullable<GraphQLConnection<WPThematicPreview>>;
};
-const thematicsListQuery = `query ThematicsList($after: String, $before: String, $first: Int, $last: Int, $orderby: [PostObjectsConnectionOrderbyInput], $search: String, $title: String) {
+const thematicsListQuery = `query ThematicsList($after: String, $before: String, $first: Int, $last: Int, $orderby: [PostObjectsConnectionOrderbyInput], $search: String, $title: String, $notIn: [ID]) {
thematics(
after: $after
before: $before
first: $first
last: $last
- where: {orderby: $orderby, search: $search, title: $title}
+ where: {orderby: $orderby, search: $search, title: $title, notIn: $notIn}
) {
edges {
cursor
diff --git a/src/services/graphql/fetchers/topics/fetch-topics-list.ts b/src/services/graphql/fetchers/topics/fetch-topics-list.ts
index 2ede721..6f2ab8f 100644
--- a/src/services/graphql/fetchers/topics/fetch-topics-list.ts
+++ b/src/services/graphql/fetchers/topics/fetch-topics-list.ts
@@ -12,13 +12,13 @@ export type TopicsListResponse = {
topics: Nullable<GraphQLConnection<WPTopicPreview>>;
};
-const topicsListQuery = `query TopicsList($after: String, $before: String, $first: Int, $last: Int, $orderby: [PostObjectsConnectionOrderbyInput], $search: String, $title: String) {
+const topicsListQuery = `query TopicsList($after: String, $before: String, $first: Int, $last: Int, $orderby: [PostObjectsConnectionOrderbyInput], $search: String, $title: String, $notIn: [ID]) {
topics(
after: $after
before: $before
first: $first
last: $last
- where: {orderby: $orderby, search: $search, title: $title}
+ where: {orderby: $orderby, search: $search, title: $title, notIn: $notIn}
) {
edges {
cursor
diff --git a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts
index 435489d..140e165 100644
--- a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts
+++ b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts
@@ -34,6 +34,7 @@ describe('convert-wp-thematic-to-thematic', () => {
const result = convertWPThematicToThematic(thematic);
expect(result.content).toBe(thematic.contentParts.afterMore);
+ expect(result.id).toBe(thematic.databaseId);
expect(result.intro).toBe(thematic.contentParts.beforeMore);
expect(result.meta.articles).toBeUndefined();
expect(result.meta.cover).toBeUndefined();
diff --git a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts
index 9aa1896..5f8d7fc 100644
--- a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts
+++ b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts
@@ -8,6 +8,7 @@ import { ROUTES } from '../../../utils/constants';
import {
getUniquePageLinks,
sortPageLinksByName,
+ updateContentTree,
} from '../../../utils/helpers';
import { convertPostPreviewToArticlePreview } from './convert-post-preview-to-article-preview';
import { convertWPTopicPreviewToPageLink } from './convert-taxonomy-to-page-link';
@@ -31,30 +32,39 @@ const getRelatedTopicsFrom = (posts: WPPostPreview[]): PageLink[] => {
return getUniquePageLinks(topics).sort(sortPageLinksByName);
};
-export const convertWPThematicToThematic = (thematic: WPThematic): Thematic => {
+export const convertWPThematicToThematic = ({
+ acfThematics,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ modified,
+ seo,
+ slug,
+ title,
+}: WPThematic): Thematic => {
return {
- content: thematic.contentParts.afterMore,
- intro: thematic.contentParts.beforeMore,
+ content: updateContentTree(contentParts.afterMore),
+ id: databaseId,
+ intro: contentParts.beforeMore,
meta: {
- articles: thematic.acfThematics?.postsInThematic?.map(
+ articles: acfThematics?.postsInThematic?.map(
convertPostPreviewToArticlePreview
),
- cover: thematic.featuredImage
- ? convertWPImgToImg(thematic.featuredImage.node)
- : undefined,
+ cover: featuredImage ? convertWPImgToImg(featuredImage.node) : undefined,
dates: {
- publication: thematic.date,
- update: thematic.modified,
+ publication: date,
+ update: modified,
},
seo: {
- description: thematic.seo.metaDesc,
- title: thematic.seo.title,
+ description: seo.metaDesc,
+ title: seo.title,
},
- relatedTopics: thematic.acfThematics?.postsInThematic
- ? getRelatedTopicsFrom(thematic.acfThematics.postsInThematic)
+ relatedTopics: acfThematics?.postsInThematic
+ ? getRelatedTopicsFrom(acfThematics.postsInThematic)
: undefined,
},
- slug: `${ROUTES.THEMATICS}/${thematic.slug}`,
- title: thematic.title,
+ slug: `${ROUTES.THEMATICS}/${slug}`,
+ title,
};
};
diff --git a/src/services/graphql/helpers/convert-wp-topic-to-topic.test.ts b/src/services/graphql/helpers/convert-wp-topic-to-topic.test.ts
index bfe2ba9..145b19d 100644
--- a/src/services/graphql/helpers/convert-wp-topic-to-topic.test.ts
+++ b/src/services/graphql/helpers/convert-wp-topic-to-topic.test.ts
@@ -34,6 +34,7 @@ describe('convert-wp-topic-to-topic', () => {
const result = convertWPTopicToTopic(topic);
expect(result.content).toBe(topic.contentParts.afterMore);
+ expect(result.id).toBe(topic.databaseId);
expect(result.intro).toBe(topic.contentParts.beforeMore);
expect(result.meta.articles).toBeUndefined();
expect(result.meta.cover).toBeUndefined();
diff --git a/src/services/graphql/helpers/convert-wp-topic-to-topic.ts b/src/services/graphql/helpers/convert-wp-topic-to-topic.ts
index b0136c7..f3ed4a9 100644
--- a/src/services/graphql/helpers/convert-wp-topic-to-topic.ts
+++ b/src/services/graphql/helpers/convert-wp-topic-to-topic.ts
@@ -26,31 +26,40 @@ const getRelatedThematicsFrom = (posts: WPPostPreview[]): PageLink[] => {
return getUniquePageLinks(thematics).sort(sortPageLinksByName);
};
-export const convertWPTopicToTopic = (topic: WPTopic): Topic => {
+export const convertWPTopicToTopic = ({
+ acfTopics,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ modified,
+ seo,
+ slug,
+ title,
+}: WPTopic): Topic => {
return {
- content: topic.contentParts.afterMore,
- intro: topic.contentParts.beforeMore,
+ content: contentParts.afterMore,
+ id: databaseId,
+ intro: contentParts.beforeMore,
meta: {
- articles: topic.acfTopics?.postsInTopic?.map(
+ articles: acfTopics?.postsInTopic?.map(
convertPostPreviewToArticlePreview
),
- cover: topic.featuredImage
- ? convertWPImgToImg(topic.featuredImage.node)
- : undefined,
+ cover: featuredImage ? convertWPImgToImg(featuredImage.node) : undefined,
dates: {
- publication: topic.date,
- update: topic.modified,
+ publication: date,
+ update: modified,
},
seo: {
- description: topic.seo.metaDesc,
- title: topic.seo.title,
+ description: seo.metaDesc,
+ title: seo.title,
},
- relatedThematics: topic.acfTopics?.postsInTopic
- ? getRelatedThematicsFrom(topic.acfTopics.postsInTopic)
+ relatedThematics: acfTopics?.postsInTopic
+ ? getRelatedThematicsFrom(acfTopics.postsInTopic)
: undefined,
- website: topic.acfTopics?.officialWebsite ?? undefined,
+ website: acfTopics?.officialWebsite ?? undefined,
},
- slug: `${ROUTES.TOPICS}/${topic.slug}`,
- title: topic.title,
+ slug: `${ROUTES.TOPICS}/${slug}`,
+ title,
};
};
diff --git a/src/styles/abstracts/placeholders/_links.scss b/src/styles/abstracts/placeholders/_links.scss
index a230e70..9bfd19e 100644
--- a/src/styles/abstracts/placeholders/_links.scss
+++ b/src/styles/abstracts/placeholders/_links.scss
@@ -29,6 +29,8 @@
}
%link-with-icon {
+ display: inline-block;
+
&::after {
display: inline-block;
content: var(--is-lang-hidden, "\0000a0" var(--lang-icon, ""))
diff --git a/src/styles/pages/blog.module.scss b/src/styles/pages/blog.module.scss
index aebf263..d1819cd 100644
--- a/src/styles/pages/blog.module.scss
+++ b/src/styles/pages/blog.module.scss
@@ -7,7 +7,7 @@
@use "partials/article-media";
@use "partials/article-wp-blocks";
-.list {
+.posts-list {
@include mix.media("screen") {
@include mix.dimensions("md") {
--col1: #{fun.convert-px(100)};
@@ -28,7 +28,7 @@
}
}
-.logo {
+.topic-logo {
max-width: fun.convert-px(50);
margin: 0 var(--spacing-xs) 0 0;
}
diff --git a/src/types/data.ts b/src/types/data.ts
index 21f773e..80a8bf3 100644
--- a/src/types/data.ts
+++ b/src/types/data.ts
@@ -273,6 +273,7 @@ export type ThematicMeta = Omit<PageMeta, 'wordsCount'> & {
};
export type Thematic = Page & {
+ id: number;
meta: ThematicMeta;
};
@@ -283,5 +284,6 @@ export type TopicMeta = Omit<PageMeta, 'wordsCount'> & {
};
export type Topic = Page & {
+ id: number;
meta: TopicMeta;
};
diff --git a/src/types/gql.ts b/src/types/gql.ts
index cec66c6..ac3ac36 100644
--- a/src/types/gql.ts
+++ b/src/types/gql.ts
@@ -70,4 +70,5 @@ export type GraphQLTaxonomyOrderBy = {
export type GraphQLTaxonomyWhere = {
search?: string;
title?: string;
+ notIn?: number[];
};
diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts
index f3bfd75..3fb0ad4 100644
--- a/src/utils/hooks/index.ts
+++ b/src/utils/hooks/index.ts
@@ -22,6 +22,8 @@ export * from './use-scroll-lock';
export * from './use-scroll-position';
export * from './use-scrollbar-width';
export * from './use-system-color-scheme';
+export * from './use-thematic';
+export * from './use-thematics-list';
export * from './use-theme';
export * from './use-timeout';
export * from './use-toggle';
diff --git a/src/utils/hooks/use-thematic/index.ts b/src/utils/hooks/use-thematic/index.ts
new file mode 100644
index 0000000..319f4b5
--- /dev/null
+++ b/src/utils/hooks/use-thematic/index.ts
@@ -0,0 +1 @@
+export * from './use-thematic';
diff --git a/src/utils/hooks/use-thematic/use-thematic.test.ts b/src/utils/hooks/use-thematic/use-thematic.test.ts
new file mode 100644
index 0000000..43d0a57
--- /dev/null
+++ b/src/utils/hooks/use-thematic/use-thematic.test.ts
@@ -0,0 +1,56 @@
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ jest,
+} from '@jest/globals';
+import { renderHook, waitFor } from '@testing-library/react';
+import { wpThematicsFixture } from '../../../../tests/fixtures';
+import { ROUTES } from '../../constants';
+import { useThematic } from './use-thematic';
+
+describe('useThematic', () => {
+ beforeEach(() => {
+ /* Not sure why it is needed, but without it Jest was complaining with
+ * `Jest worker encountered 4 child process exceptions`... Maybe because of
+ * useSWR? */
+ jest.useFakeTimers({
+ doNotFake: ['queueMicrotask'],
+ });
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ /* eslint-disable max-statements */
+ it('fetch the requested thematic', async () => {
+ const { result } = renderHook(() =>
+ useThematic(wpThematicsFixture[0].slug)
+ );
+
+ // Inaccurate assertions count because of waitFor...
+ //expect.assertions(8);
+ expect.hasAssertions();
+
+ expect(result.current.thematic).toBeUndefined();
+ expect(result.current.isError).toBe(false);
+ expect(result.current.isLoading).toBe(true);
+ expect(result.current.isValidating).toBe(true);
+
+ jest.advanceTimersToNextTimer();
+
+ await waitFor(() =>
+ expect(result.current.thematic?.slug).toBe(
+ `${ROUTES.THEMATICS}/${wpThematicsFixture[0].slug}`
+ )
+ );
+ expect(result.current.isError).toBe(false);
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.isValidating).toBe(false);
+ });
+ /* eslint-enable max-statements */
+});
diff --git a/src/utils/hooks/use-thematic/use-thematic.ts b/src/utils/hooks/use-thematic/use-thematic.ts
new file mode 100644
index 0000000..68127d2
--- /dev/null
+++ b/src/utils/hooks/use-thematic/use-thematic.ts
@@ -0,0 +1,31 @@
+import useSWR from 'swr';
+import {
+ convertWPThematicToThematic,
+ fetchThematic,
+} from '../../../services/graphql';
+import type { Maybe, Thematic, WPThematic } from '../../../types';
+
+export type UseThematicReturn<T extends Maybe<WPThematic>> = {
+ isError: boolean;
+ isLoading: boolean;
+ isValidating: boolean;
+ thematic: T extends undefined ? Maybe<Thematic> : Thematic;
+};
+
+export const useThematic = <T extends Maybe<WPThematic>>(
+ slug: string,
+ fallback?: T
+): UseThematicReturn<T> => {
+ const { data, error, isLoading, isValidating } = useSWR(slug, fetchThematic, {
+ fallbackData: fallback,
+ });
+
+ if (error) console.error(error);
+
+ return {
+ isError: !!error,
+ isLoading,
+ isValidating,
+ thematic: data ? convertWPThematicToThematic(data) : undefined,
+ } as UseThematicReturn<T>;
+};
diff --git a/src/utils/hooks/use-thematics-list/index.ts b/src/utils/hooks/use-thematics-list/index.ts
new file mode 100644
index 0000000..a886017
--- /dev/null
+++ b/src/utils/hooks/use-thematics-list/index.ts
@@ -0,0 +1 @@
+export * from './use-thematics-list';
diff --git a/src/utils/hooks/use-thematics-list/use-thematics-list.test.ts b/src/utils/hooks/use-thematics-list/use-thematics-list.test.ts
new file mode 100644
index 0000000..0e19c2d
--- /dev/null
+++ b/src/utils/hooks/use-thematics-list/use-thematics-list.test.ts
@@ -0,0 +1,48 @@
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ jest,
+} from '@jest/globals';
+import { renderHook, waitFor } from '@testing-library/react';
+import { useThematicsList } from './use-thematics-list';
+
+describe('useThematicsList', () => {
+ beforeEach(() => {
+ /* Not sure why it is needed, but without it Jest was complaining with
+ * `Jest worker encountered 4 child process exceptions`... Maybe because of
+ * useSWR? */
+ jest.useFakeTimers({
+ doNotFake: ['queueMicrotask'],
+ });
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ /* eslint-disable max-statements */
+ it('fetch the requested thematics list', async () => {
+ const { result } = renderHook(() => useThematicsList());
+
+ // Inaccurate assertions count because of waitFor...
+ //expect.assertions(8);
+ expect.hasAssertions();
+
+ expect(result.current.thematics).toBeUndefined();
+ expect(result.current.isError).toBe(false);
+ expect(result.current.isLoading).toBe(true);
+ expect(result.current.isValidating).toBe(true);
+
+ jest.advanceTimersToNextTimer();
+
+ await waitFor(() => expect(result.current.thematics).toBeDefined());
+ expect(result.current.isError).toBe(false);
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.isValidating).toBe(false);
+ });
+ /* eslint-enable max-statements */
+});
diff --git a/src/utils/hooks/use-thematics-list/use-thematics-list.ts b/src/utils/hooks/use-thematics-list/use-thematics-list.ts
new file mode 100644
index 0000000..f63815a
--- /dev/null
+++ b/src/utils/hooks/use-thematics-list/use-thematics-list.ts
@@ -0,0 +1,50 @@
+import useSWR from 'swr';
+import {
+ type FetchThematicsListInput,
+ fetchThematicsList,
+} from '../../../services/graphql';
+import type {
+ GraphQLConnection,
+ Maybe,
+ WPThematicPreview,
+} from '../../../types';
+
+export type UseThematicsListReturn<
+ T extends Maybe<GraphQLConnection<WPThematicPreview>>,
+> = {
+ isError: boolean;
+ isLoading: boolean;
+ isValidating: boolean;
+ thematics: T extends undefined
+ ? Maybe<GraphQLConnection<WPThematicPreview>>
+ : GraphQLConnection<WPThematicPreview>;
+};
+
+export type UseThematicsListConfig<
+ T extends Maybe<GraphQLConnection<WPThematicPreview>>,
+> = {
+ input?: FetchThematicsListInput;
+ fallback?: T;
+};
+
+export const useThematicsList = <
+ T extends Maybe<GraphQLConnection<WPThematicPreview>>,
+>(
+ config?: UseThematicsListConfig<T>
+): UseThematicsListReturn<T> => {
+ const { fallback, input } = config ?? {};
+ const { data, error, isLoading, isValidating } = useSWR(
+ input ?? {},
+ fetchThematicsList,
+ { fallbackData: fallback }
+ );
+
+ if (error) console.error(error);
+
+ return {
+ isError: !!error,
+ isLoading,
+ isValidating,
+ thematics: data,
+ } as UseThematicsListReturn<T>;
+};
diff --git a/tests/cypress/e2e/pages/thematic.cy.ts b/tests/cypress/e2e/pages/thematic.cy.ts
new file mode 100644
index 0000000..dab0d45
--- /dev/null
+++ b/tests/cypress/e2e/pages/thematic.cy.ts
@@ -0,0 +1,41 @@
+import { ROUTES } from '../../../../src/utils/constants';
+
+describe('Thematic', () => {
+ beforeEach(() => {
+ cy.visit(ROUTES.HOME);
+ cy.findByRole('link', { name: /^Développement/i }).click();
+ });
+
+ it('successfully loads', () => {
+ cy.findByRole('heading', { level: 1 }).should('exist');
+ });
+
+ it('contains the thematic meta', () => {
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ cy.findAllByRole('term').should('have.length.at.least', 2);
+
+ /* The accessible name is not recognized while it should be the `dt` text
+ * content */
+ /* cy.findByRole('term', { name: 'Publié le :' }).should('exist');
+ cy.findByRole('term', { name: 'Total :' }).should('exist'); */
+ });
+
+ it('contains a breadcrumbs', () => {
+ cy.findByRole('navigation', { name: 'Fil d’Ariane' }).should('exist');
+ });
+
+ it('contains a table of contents', () => {
+ cy.findByRole('heading', { level: 2, name: 'Table des matières' }).should(
+ 'exist'
+ );
+ });
+
+ it('contains a thematics list widget and a topics list widget', () => {
+ cy.findByRole('heading', { level: 2, name: 'Autres thématiques' }).should(
+ 'exist'
+ );
+ cy.findByRole('heading', { level: 2, name: 'Sujets connexes' }).should(
+ 'exist'
+ );
+ });
+});
diff --git a/tests/msw/handlers/thematics/thematics-list.handler.ts b/tests/msw/handlers/thematics/thematics-list.handler.ts
index f206247..7afec4c 100644
--- a/tests/msw/handlers/thematics/thematics-list.handler.ts
+++ b/tests/msw/handlers/thematics/thematics-list.handler.ts
@@ -23,12 +23,17 @@ export const thematicsListHandler = graphql.query<
variableValues: variables,
rootValue: {
thematics({ after, first, where }: typeof variables) {
- const { search, title } = where ?? {};
+ const { notIn, search, title } = where ?? {};
+ const filteredThematicsById = notIn
+ ? wpThematicsFixture.filter(
+ (thematic) => !notIn.includes(thematic.databaseId)
+ )
+ : wpThematicsFixture;
const filteredThematicsByTitle = title
- ? wpThematicsFixture.filter((thematic) =>
+ ? filteredThematicsById.filter((thematic) =>
thematic.title.includes(title)
)
- : wpThematicsFixture;
+ : filteredThematicsById;
const filteredThematics = search
? filteredThematicsByTitle.filter((thematic) =>
thematic.title.includes(search)
diff --git a/tests/msw/handlers/topics/topics-list.handler.ts b/tests/msw/handlers/topics/topics-list.handler.ts
index 5e3e31a..4b09c5a 100644
--- a/tests/msw/handlers/topics/topics-list.handler.ts
+++ b/tests/msw/handlers/topics/topics-list.handler.ts
@@ -23,10 +23,13 @@ export const topicsListHandler = graphql.query<
variableValues: variables,
rootValue: {
topics({ after, first, where }: typeof variables) {
- const { search, title } = where ?? {};
- const filteredTopicsByTitle = title
- ? wpTopicsFixture.filter((topic) => topic.title.includes(title))
+ const { notIn, search, title } = where ?? {};
+ const filteredTopicsById = notIn
+ ? wpTopicsFixture.filter((topic) => !notIn.includes(topic.databaseId))
: wpTopicsFixture;
+ const filteredTopicsByTitle = title
+ ? filteredTopicsById.filter((topic) => topic.title.includes(title))
+ : filteredTopicsById;
const filteredTopics = search
? filteredTopicsByTitle.filter((topic) =>
topic.title.includes(search)
diff --git a/tests/msw/schema/types/thematic.types.ts b/tests/msw/schema/types/thematic.types.ts
index 2af4f9a..4f8e841 100644
--- a/tests/msw/schema/types/thematic.types.ts
+++ b/tests/msw/schema/types/thematic.types.ts
@@ -26,6 +26,7 @@ type Thematic {
input RootQueryToThematicConnectionWhereArgs {
authorName: String
+ notIn: [ID]
orderby: [PostObjectsConnectionOrderbyInput]
search: String
title: String
diff --git a/tests/msw/schema/types/topic.types.ts b/tests/msw/schema/types/topic.types.ts
index 2d54653..7d6e4a0 100644
--- a/tests/msw/schema/types/topic.types.ts
+++ b/tests/msw/schema/types/topic.types.ts
@@ -27,6 +27,7 @@ type Topic {
input RootQueryToTopicConnectionWhereArgs {
authorName: String
+ notIn: [ID]
orderby: [PostObjectsConnectionOrderbyInput]
search: String
title: String