diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/atoms/links/social-link/social-link.test.tsx | 2 | ||||
| -rw-r--r-- | src/components/organisms/forms/search-form/search-form.test.tsx | 2 | ||||
| -rw-r--r-- | src/components/templates/page/page-layout.tsx | 19 | ||||
| -rw-r--r-- | src/pages/blog/page/[number].tsx | 14 | ||||
| -rw-r--r-- | src/services/graphql/articles.ts | 35 | ||||
| -rw-r--r-- | src/services/graphql/thematics.ts | 40 | ||||
| -rw-r--r-- | src/services/graphql/topics.ts | 36 | ||||
| -rw-r--r-- | src/utils/helpers/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/helpers/pages.tsx | 8 | ||||
| -rw-r--r-- | src/utils/helpers/rehype.ts | 23 | ||||
| -rw-r--r-- | src/utils/helpers/rss.ts | 4 | ||||
| -rw-r--r-- | src/utils/hooks/use-article.ts | 18 | ||||
| -rw-r--r-- | src/utils/hooks/use-headings-tree/use-headings-tree.test.ts | 47 | ||||
| -rw-r--r-- | src/utils/hooks/use-headings-tree/use-headings-tree.ts | 36 | ||||
| -rw-r--r-- | src/utils/hooks/use-posts-list/use-posts-list.ts | 11 |
15 files changed, 179 insertions, 117 deletions
diff --git a/src/components/atoms/links/social-link/social-link.test.tsx b/src/components/atoms/links/social-link/social-link.test.tsx index 9129c27..041e150 100644 --- a/src/components/atoms/links/social-link/social-link.test.tsx +++ b/src/components/atoms/links/social-link/social-link.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, it } from '@jest/globals'; +import { describe, expect, it, jest } from '@jest/globals'; import { render, screen as rtlScreen } from '@testing-library/react'; import { SocialLink } from './social-link'; diff --git a/src/components/organisms/forms/search-form/search-form.test.tsx b/src/components/organisms/forms/search-form/search-form.test.tsx index 56ba0d7..d1fdfa9 100644 --- a/src/components/organisms/forms/search-form/search-form.test.tsx +++ b/src/components/organisms/forms/search-form/search-form.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, it } from '@jest/globals'; +import { describe, expect, it, jest } from '@jest/globals'; import { userEvent } from '@testing-library/user-event'; import { render, screen as rtlScreen } from '../../../../../tests/utils'; import { SearchForm } from './search-form'; diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx index 8ea0087..db71e07 100644 --- a/src/components/templates/page/page-layout.tsx +++ b/src/components/templates/page/page-layout.tsx @@ -4,14 +4,13 @@ import { type FC, type HTMLAttributes, type ReactNode, - useRef, useCallback, } from 'react'; import { useIntl } from 'react-intl'; import type { BreadcrumbList } from 'schema-dts'; import { sendComment } from '../../../services/graphql'; import type { SendCommentInput } from '../../../types'; -import { useHeadingsTree, useIsMounted } from '../../../utils/hooks'; +import { useHeadingsTree } from '../../../utils/hooks'; import { Heading, Sidebar } from '../../atoms'; import { PageFooter, @@ -137,9 +136,9 @@ export const PageLayout: FC<PageLayoutProps> = ({ id: 'eys2uX', }); - const bodyRef = useRef<HTMLDivElement>(null); - const isMounted = useIsMounted(bodyRef); - const headingsTree = useHeadingsTree(bodyRef, { fromLevel: 2 }); + const { ref: bodyRef, tree: headingsTree } = useHeadingsTree<HTMLDivElement>({ + fromLevel: 2, + }); const saveComment: CommentFormSubmit = useCallback( async (data) => { @@ -223,12 +222,10 @@ export const PageLayout: FC<PageLayoutProps> = ({ })} className={`${styles.sidebar} ${styles['sidebar--first']}`} > - {isMounted && bodyRef.current ? ( - <TocWidget - heading={<Heading level={3}>{tocTitle}</Heading>} - tree={headingsTree} - /> - ) : null} + <TocWidget + heading={<Heading level={3}>{tocTitle}</Heading>} + tree={headingsTree} + /> </Sidebar> ) : null} {typeof children === 'string' ? ( diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx index 49b5eb4..03b641b 100644 --- a/src/pages/blog/page/[number].tsx +++ b/src/pages/blog/page/[number].tsx @@ -39,12 +39,15 @@ import { getBlogSchema, getLinksItemData, getPageLinkFromRawData, - getPostsList, getSchemaJson, getWebPageSchema, } from '../../../utils/helpers'; import { loadTranslation, type Messages } from '../../../utils/helpers/server'; -import { useBreadcrumb, useRedirection } from '../../../utils/hooks'; +import { + useBreadcrumb, + usePostsList, + useRedirection, +} from '../../../utils/hooks'; type BlogPageProps = { articles: EdgesResponse<RawArticle>; @@ -70,6 +73,11 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ redirectTo: ROUTES.BLOG, }); + const { posts } = usePostsList({ + fallback: [articles], + fetcher: getArticles, + perPage: CONFIG.postsPerPage, + }); const intl = useIntl(); const title = intl.formatMessage({ defaultMessage: 'Blog', @@ -260,7 +268,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ />, ]} > - <PostsList posts={getPostsList([articles])} sortByYear /> + <PostsList posts={posts ?? []} sortByYear /> <Pagination aria-label={paginationAriaLabel} current={pageNumber} diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts index 789ef2b..82bde41 100644 --- a/src/services/graphql/articles.ts +++ b/src/services/graphql/articles.ts @@ -1,19 +1,20 @@ -import { - type Article, - type ArticleCard, - type EdgesResponse, - type EndCursorResponse, - type GraphQLEdgesInput, - type GraphQLPageInfo, - type RawArticle, - type RawArticlePreview, - type Slug, - type TotalItems, +import type { + Article, + ArticleCard, + EdgesResponse, + EndCursorResponse, + GraphQLEdgesInput, + GraphQLPageInfo, + RawArticle, + RawArticlePreview, + Slug, + TotalItems, } from '../../types'; import { getAuthorFromRawData, getImageFromRawData, getPageLinkFromRawData, + updateContentTree, } from '../../utils/helpers'; import { fetchAPI } from './api'; import { @@ -50,7 +51,9 @@ export type GetArticlesReturn = { * @param {RawArticle} data - The page raw data. * @returns {Article} The page data. */ -export const getArticleFromRawData = (data: RawArticle): Article => { +export const getArticleFromRawData = async ( + data: RawArticle +): Promise<Article> => { const { acfPosts, author, @@ -67,19 +70,19 @@ export const getArticleFromRawData = (data: RawArticle): Article => { } = data; return { - content: contentParts.afterMore, + content: await updateContentTree(contentParts.afterMore), id: databaseId, intro: contentParts.beforeMore, meta: { author: author && getAuthorFromRawData(author.node, 'page'), - commentsCount: commentCount || 0, + commentsCount: commentCount ?? 0, cover: featuredImage?.node ? getImageFromRawData(featuredImage.node) : undefined, dates: { publication: date, update: modified }, seo: { - description: seo?.metaDesc || '', - title: seo?.title || '', + description: seo?.metaDesc ?? '', + title: seo?.title ?? '', }, thematics: acfPosts.postsInThematic?.map((thematic) => getPageLinkFromRawData(thematic, 'thematic') diff --git a/src/services/graphql/thematics.ts b/src/services/graphql/thematics.ts index 7a57824..c02a42c 100644 --- a/src/services/graphql/thematics.ts +++ b/src/services/graphql/thematics.ts @@ -1,13 +1,13 @@ -import { - type EdgesResponse, - type GraphQLEdgesInput, - type PageLink, - type RawArticle, - type RawThematic, - type RawThematicPreview, - type Slug, - type Thematic, - type TotalItems, +import type { + EdgesResponse, + GraphQLEdgesInput, + PageLink, + RawArticle, + RawThematic, + RawThematicPreview, + Slug, + Thematic, + TotalItems, } from '../../types'; import { getImageFromRawData, @@ -59,7 +59,9 @@ export const getThematicsPreview = async ( * @param {RawThematic} data - The page raw data. * @returns {Thematic} The page data. */ -export const getThematicFromRawData = (data: RawThematic): Thematic => { +export const getThematicFromRawData = async ( + data: RawThematic +): Promise<Thematic> => { const { acfThematics, contentParts, @@ -84,9 +86,9 @@ export const getThematicFromRawData = (data: RawThematic): Thematic => { posts.forEach((post) => { if (post.acfPosts.postsInTopic) { - post.acfPosts.postsInTopic.forEach((topic) => - topics.push(getPageLinkFromRawData(topic, 'topic')) - ); + for (const topic of post.acfPosts.postsInTopic) { + topics.push(getPageLinkFromRawData(topic, 'topic')); + } } }); @@ -103,16 +105,18 @@ export const getThematicFromRawData = (data: RawThematic): Thematic => { id: databaseId, intro: contentParts.beforeMore, meta: { - articles: acfThematics.postsInThematic.map((post) => - getArticleFromRawData(post) + articles: await Promise.all( + acfThematics.postsInThematic.map(async (post) => + getArticleFromRawData(post) + ) ), cover: featuredImage?.node ? getImageFromRawData(featuredImage.node) : undefined, dates: { publication: date, update: modified }, seo: { - description: seo?.metaDesc || '', - title: seo?.title || '', + description: seo?.metaDesc ?? '', + title: seo?.title ?? '', }, topics: getRelatedTopics(acfThematics.postsInThematic), wordsCount: info.wordsCount, diff --git a/src/services/graphql/topics.ts b/src/services/graphql/topics.ts index 921b10d..d8a9b6a 100644 --- a/src/services/graphql/topics.ts +++ b/src/services/graphql/topics.ts @@ -1,13 +1,13 @@ -import { - type EdgesResponse, - type GraphQLEdgesInput, - type PageLink, - type RawArticle, - type RawTopic, - type RawTopicPreview, - type Slug, - type Topic, - type TotalItems, +import type { + EdgesResponse, + GraphQLEdgesInput, + PageLink, + RawArticle, + RawTopic, + RawTopicPreview, + Slug, + Topic, + TotalItems, } from '../../types'; import { getImageFromRawData, @@ -59,7 +59,7 @@ export const getTopicsPreview = async ( * @param {RawTopic} data - The page raw data. * @returns {Topic} The page data. */ -export const getTopicFromRawData = (data: RawTopic): Topic => { +export const getTopicFromRawData = async (data: RawTopic): Promise<Topic> => { const { acfTopics, contentParts, @@ -84,9 +84,9 @@ export const getTopicFromRawData = (data: RawTopic): Topic => { posts.forEach((post) => { if (post.acfPosts.postsInThematic) { - post.acfPosts.postsInThematic.forEach((thematic) => - thematics.push(getPageLinkFromRawData(thematic, 'thematic')) - ); + for (const thematic of post.acfPosts.postsInThematic) { + thematics.push(getPageLinkFromRawData(thematic, 'thematic')); + } } }); @@ -103,8 +103,8 @@ export const getTopicFromRawData = (data: RawTopic): Topic => { id: databaseId, intro: contentParts.beforeMore, meta: { - articles: acfTopics.postsInTopic.map((post) => - getArticleFromRawData(post) + articles: await Promise.all( + acfTopics.postsInTopic.map(async (post) => getArticleFromRawData(post)) ), cover: featuredImage?.node ? getImageFromRawData(featuredImage.node) @@ -112,8 +112,8 @@ export const getTopicFromRawData = (data: RawTopic): Topic => { dates: { publication: date, update: modified }, website: acfTopics.officialWebsite, seo: { - description: seo?.metaDesc || '', - title: seo?.title || '', + description: seo?.metaDesc ?? '', + title: seo?.title ?? '', }, thematics: getRelatedThematics(acfTopics.postsInTopic), wordsCount: info.wordsCount, diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 79077de..92f9424 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -3,6 +3,7 @@ export * from './images'; export * from './pages'; export * from './reading-time'; export * from './refs'; +export * from './rehype'; export * from './rss'; export * from './schema-org'; export * from './strings'; diff --git a/src/utils/helpers/pages.tsx b/src/utils/helpers/pages.tsx index 62a582f..7b6bdca 100644 --- a/src/utils/helpers/pages.tsx +++ b/src/utils/helpers/pages.tsx @@ -109,9 +109,9 @@ export const getPostsWithUrl = (posts: Article[]): PostData[] => * @param {EdgesResponse<RawArticle>[]} rawData - The raw data. * @returns {PostData[]} An array of posts. */ -export const getPostsList = ( +export const getPostsList = async ( rawData: EdgesResponse<RawArticle>[] -): PostData[] => { +): Promise<PostData[]> => { const articlesList: RawArticle[] = []; rawData.forEach((articleData) => { articleData.edges.forEach((edge) => { @@ -120,6 +120,8 @@ export const getPostsList = ( }); return getPostsWithUrl( - articlesList.map((article) => getArticleFromRawData(article)) + await Promise.all( + articlesList.map(async (article) => getArticleFromRawData(article)) + ) ); }; diff --git a/src/utils/helpers/rehype.ts b/src/utils/helpers/rehype.ts new file mode 100644 index 0000000..2716c62 --- /dev/null +++ b/src/utils/helpers/rehype.ts @@ -0,0 +1,23 @@ +/** + * Update a stringified HTML tree using unified plugins. + * + * It will parse the provided content to add id to each headings. + * + * @param {string} content - The page contents. + * @returns {string} The updated page contents. + */ +export const updateContentTree = async (content: string): Promise<string> => { + const { unified } = await import('unified'); + const rehypeParse = (await import('rehype-parse')).default; + const rehypeSanitize = (await import('rehype-sanitize')).default; + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeStringify = (await import('rehype-stringify')).default; + + return unified() + .use(rehypeParse, { fragment: true }) + .use(rehypeSlug) + .use(() => rehypeSanitize({ clobberPrefix: 'h-' })) + .use(rehypeStringify) + .processSync(content) + .toString(); +}; diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts index 6de60cc..d9c3b1e 100644 --- a/src/utils/helpers/rss.ts +++ b/src/utils/helpers/rss.ts @@ -18,8 +18,8 @@ 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(async (edge) => { + articles.push(await getArticleFromRawData(edge.node)); }); return articles; diff --git a/src/utils/hooks/use-article.ts b/src/utils/hooks/use-article.ts index 5cf0e51..f339f7f 100644 --- a/src/utils/hooks/use-article.ts +++ b/src/utils/hooks/use-article.ts @@ -1,10 +1,11 @@ +import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { articleBySlugQuery, fetchAPI, getArticleFromRawData, } from '../../services/graphql'; -import type { Article, RawArticle } from '../../types'; +import type { Article, Maybe, RawArticle } from '../../types'; export type UseArticleConfig = { /** @@ -32,6 +33,19 @@ export const useArticle = ({ fetchAPI<RawArticle, typeof articleBySlugQuery>, {} ); + const [article, setArticle] = useState<Maybe<Article>>(); - return data ? getArticleFromRawData(data.post) : fallback; + useEffect(() => { + const getArticle = async () => { + if (data) { + setArticle(await getArticleFromRawData(data.post)); + } else { + setArticle(fallback); + } + }; + + getArticle(); + }, [data, fallback]); + + return article; }; diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts index ad30a4f..2c8ff2d 100644 --- a/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts +++ b/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { renderHook } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react'; import { useHeadingsTree } from './use-headings-tree'; const labels = { @@ -9,7 +9,7 @@ const labels = { }; describe('useHeadingsTree', () => { - it('returns a ref object and the headings tree', () => { + it('returns a ref callback and the headings tree', () => { const wrapper = document.createElement('div'); wrapper.innerHTML = ` @@ -19,12 +19,13 @@ describe('useHeadingsTree', () => { <h2>${labels.secondH2}</h2> <p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`; - const wrapperRef = { current: wrapper }; - const { result } = renderHook(() => useHeadingsTree(wrapperRef)); + const { result } = renderHook(() => useHeadingsTree()); - expect(result.current.length).toBe(1); - expect(result.current[0].label).toBe(labels.h1); - expect(result.current[0].children.length).toBe(2); + act(() => result.current.ref(wrapper)); + + expect(result.current.tree.length).toBe(1); + expect(result.current.tree[0].label).toBe(labels.h1); + expect(result.current.tree[0].children.length).toBe(2); }); it('can return a headings tree starting at the specified level', () => { @@ -37,14 +38,13 @@ describe('useHeadingsTree', () => { <h2>${labels.secondH2}</h2> <p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`; - const wrapperRef = { current: wrapper }; - const { result } = renderHook(() => - useHeadingsTree(wrapperRef, { fromLevel: 2 }) - ); + const { result } = renderHook(() => useHeadingsTree({ fromLevel: 2 })); + + act(() => result.current.ref(wrapper)); - expect(result.current.length).toBe(2); - expect(result.current[0].label).toBe(labels.firstH2); - expect(result.current[1].label).toBe(labels.secondH2); + expect(result.current.tree.length).toBe(2); + expect(result.current.tree[0].label).toBe(labels.firstH2); + expect(result.current.tree[1].label).toBe(labels.secondH2); }); it('can return a headings tree stopping at the specified level', () => { @@ -57,22 +57,17 @@ describe('useHeadingsTree', () => { <h2>${labels.secondH2}</h2> <p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`; - const wrapperRef = { current: wrapper }; - const { result } = renderHook(() => - useHeadingsTree(wrapperRef, { toLevel: 1 }) - ); + const { result } = renderHook(() => useHeadingsTree({ toLevel: 1 })); + + act(() => result.current.ref(wrapper)); - expect(result.current.length).toBe(1); - expect(result.current[0].label).toBe(labels.h1); - expect(result.current[0].children).toStrictEqual([]); + expect(result.current.tree.length).toBe(1); + expect(result.current.tree[0].label).toBe(labels.h1); + expect(result.current.tree[0].children).toStrictEqual([]); }); it('throws an error if the options are invalid', () => { - const wrapperRef = { current: null }; - - expect(() => - useHeadingsTree(wrapperRef, { fromLevel: 2, toLevel: 1 }) - ).toThrowError( + expect(() => useHeadingsTree({ fromLevel: 2, toLevel: 1 })).toThrowError( 'Invalid options: `fromLevel` must be lower or equal to `toLevel`.' ); }); diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.ts index 6a081e7..68bdde8 100644 --- a/src/utils/hooks/use-headings-tree/use-headings-tree.ts +++ b/src/utils/hooks/use-headings-tree/use-headings-tree.ts @@ -1,4 +1,4 @@ -import { useEffect, useState, type RefObject } from 'react'; +import { useState, useCallback, type RefCallback } from 'react'; import type { HeadingLevel } from '../../../components'; export type HeadingsTreeNode = { @@ -111,17 +111,26 @@ const buildHeadingsTreeFrom = ( return treeNodes; }; +export type UseHeadingsTreeReturn<T extends HTMLElement> = { + /** + * A callback function to set a ref. + */ + ref: RefCallback<T>; + /** + * The headings tree. + */ + tree: HeadingsTreeNode[]; +}; + /** * React hook to retrieve the headings tree in a document or in a given wrapper. * - * @param {RefObject<T>} ref - A ref to the element where to look for headings. * @param {UseHeadingsTreeOptions} options - The headings tree config. - * @returns {HeadingsTreeNode[]} The headings tree. + * @returns {UseHeadingsTreeReturn<T>} The headings tree and a ref callback. */ export const useHeadingsTree = <T extends HTMLElement = HTMLElement>( - ref: RefObject<T>, options?: UseHeadingsTreeOptions -): HeadingsTreeNode[] => { +): UseHeadingsTreeReturn<T> => { if ( options?.fromLevel && options.toLevel && @@ -134,15 +143,14 @@ export const useHeadingsTree = <T extends HTMLElement = HTMLElement>( const [tree, setTree] = useState<HeadingsTreeNode[]>([]); const requestedHeadingTags = getHeadingTagsList(options); const query = requestedHeadingTags.join(', '); + const ref: RefCallback<T> = useCallback( + (el) => { + const headingNodes = el?.querySelectorAll<HTMLHeadingElement>(query); - useEffect(() => { - if (typeof window === 'undefined') return; - - const headingNodes = - ref.current?.querySelectorAll<HTMLHeadingElement>(query); - - if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes)); - }, [query, ref]); + if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes)); + }, + [query] + ); - return tree; + return { ref, tree }; }; diff --git a/src/utils/hooks/use-posts-list/use-posts-list.ts b/src/utils/hooks/use-posts-list/use-posts-list.ts index 661727f..980d531 100644 --- a/src/utils/hooks/use-posts-list/use-posts-list.ts +++ b/src/utils/hooks/use-posts-list/use-posts-list.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import type { PostData } from '../../../components'; import type { Maybe, RawArticle } from '../../../types'; import { getPostsList } from '../../helpers'; @@ -40,8 +40,15 @@ export const usePostsList = ( } = usePagination(config); const [firstNewResultIndex, setFirstNewResultIndex] = useState<Maybe<number>>(undefined); + const [posts, setPosts] = useState<Maybe<PostData[]>>(undefined); - const posts = data ? getPostsList(data) : undefined; + useEffect(() => { + const getPosts = async () => { + if (data) setPosts(await getPostsList(data)); + }; + + getPosts(); + }, [data]); const handleLoadMore = useCallback(async () => { setFirstNewResultIndex(size * config.perPage + 1); |
