diff options
| -rw-r--r-- | next.config.js | 3 | ||||
| -rw-r--r-- | package.json | 5 | ||||
| -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 | ||||
| -rw-r--r-- | yarn.lock | 181 |
18 files changed, 365 insertions, 120 deletions
diff --git a/next.config.js b/next.config.js index d620718..8227603 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,7 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import bundleAnalyzer from '@next/bundle-analyzer'; import nextMDX from '@next/mdx'; +import rehypeSlug from 'rehype-slug'; const currentDir = dirname(fileURLToPath(import.meta.url)); @@ -164,7 +165,7 @@ const withMDX = nextMDX({ extension: /\.mdx?$/, options: { remarkPlugins: [], - rehypePlugins: [], + rehypePlugins: [rehypeSlug], }, }); diff --git a/package.json b/package.json index baf8fc3..c595c9f 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,14 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-intl": "^6.5.5", + "rehype-parse": "^9.0.0", + "rehype-sanitize": "^6.0.0", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.0", "schema-dts": "^1.1.2", "sharp": "^0.32.6", "swr": "^2.2.4", + "unified": "^11.0.4", "use-ackee": "^3.0.1" }, "devDependencies": { 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); @@ -8582,6 +8582,11 @@ github-slugger@^1.0.0: resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== +github-slugger@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a" + integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -8828,6 +8833,74 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +hast-util-from-html@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz#9cd38ee81bf40b2607368b92a04b0905fa987488" + integrity sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.1.0" + hast-util-from-parse5 "^8.0.0" + parse5 "^7.0.0" + vfile "^6.0.0" + vfile-message "^4.0.0" + +hast-util-from-parse5@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" + integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^8.0.0" + property-information "^6.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + +hast-util-heading-rank@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz#2d5c6f2807a7af5c45f74e623498dd6054d2aba8" + integrity sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-raw@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.1.tgz#2ba8510e4ed2a1e541cde2a4ebb5c38ab4c82c2d" + integrity sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-from-parse5 "^8.0.0" + hast-util-to-parse5 "^8.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + parse5 "^7.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-sanitize@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-5.0.1.tgz#8e90068cd68e651c569960b77a1b25076579b4cf" + integrity sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ== + dependencies: + "@types/hast" "^3.0.0" + "@ungap/structured-clone" "^1.2.0" + unist-util-position "^5.0.0" + hast-util-to-estree@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz#f2afe5e869ddf0cf690c75f9fc699f3180b51b19" @@ -8850,6 +8923,24 @@ hast-util-to-estree@^3.0.0: unist-util-position "^5.0.0" zwitch "^2.0.0" +hast-util-to-html@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz#51c0ae2a3550b9aa988c094c4fc4e327af0dddd1" + integrity sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-raw "^9.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + hast-util-to-jsx-runtime@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.2.0.tgz#ffd59bfcf0eb8321c6ed511bfc4b399ac3404bc2" @@ -8865,6 +8956,26 @@ hast-util-to-jsx-runtime@^2.0.0: unist-util-position "^5.0.0" vfile-message "^4.0.0" +hast-util-to-parse5@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" + integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-to-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz#2a131948b4b1b26461a2c8ac876e2c88d02946bd" + integrity sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA== + dependencies: + "@types/hast" "^3.0.0" + hast-util-whitespace@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" @@ -8872,6 +8983,17 @@ hast-util-whitespace@^3.0.0: dependencies: "@types/hast" "^3.0.0" +hastscript@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a" + integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -8940,6 +9062,11 @@ html-tags@^3.1.0, html-tags@^3.3.1: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + html-webpack-plugin@^5.5.0: version "5.5.3" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz#72270f4a78e222b5825b296e5e3e1328ad525a3e" @@ -12778,6 +12905,43 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" +rehype-parse@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-9.0.0.tgz#3949faeec6f466ec57774215661e0d75469195d9" + integrity sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw== + dependencies: + "@types/hast" "^3.0.0" + hast-util-from-html "^2.0.0" + unified "^11.0.0" + +rehype-sanitize@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz#16e95f4a67a69cbf0f79e113c8e0df48203db73c" + integrity sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg== + dependencies: + "@types/hast" "^3.0.0" + hast-util-sanitize "^5.0.0" + +rehype-slug@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/rehype-slug/-/rehype-slug-6.0.0.tgz#1d21cf7fc8a83ef874d873c15e6adaee6344eaf1" + integrity sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A== + dependencies: + "@types/hast" "^3.0.0" + github-slugger "^2.0.0" + hast-util-heading-rank "^3.0.0" + hast-util-to-string "^3.0.0" + unist-util-visit "^5.0.0" + +rehype-stringify@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-10.0.0.tgz#2031cf6fdd0355393706f0474ec794c75e5492f2" + integrity sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ== + dependencies: + "@types/hast" "^3.0.0" + hast-util-to-html "^9.0.0" + unified "^11.0.0" + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -14361,7 +14525,7 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unified@^11.0.0: +unified@^11.0.0, unified@^11.0.4: version "11.0.4" resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== @@ -14625,6 +14789,14 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-location@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464" + integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + vfile-message@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" @@ -14686,6 +14858,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -15058,7 +15235,7 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== -zwitch@^2.0.0: +zwitch@^2.0.0, zwitch@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== |
