diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-08-21 13:50:18 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-08-21 13:50:18 +0200 |
| commit | a3eb518dcccaebd0f48c708c189ad2fcb07f0f73 (patch) | |
| tree | 21d8350b85f47c41c382ef64ce0b91003d363a84 | |
| parent | a0d00743cbbdb77b27c1a3d5711407ffed5befac (diff) | |
fix(comments): load all comments on a post
Previously, only the first 10 comments was loaded. So I update the
fetching method to retrieve all the comments on a post.
Also, I choose to order comments on client side because of a bug
with WPGraphQL.
Finally, I renamed the Comment type to SingleComment to avoid conflict
with existing types.
| -rw-r--r-- | src/components/organisms/layout/comment.tsx | 4 | ||||
| -rw-r--r-- | src/components/organisms/layout/comments-list.fixture.tsx | 4 | ||||
| -rw-r--r-- | src/components/organisms/layout/comments-list.tsx | 12 | ||||
| -rw-r--r-- | src/pages/article/[slug].tsx | 8 | ||||
| -rw-r--r-- | src/services/graphql/comments.query.ts | 37 | ||||
| -rw-r--r-- | src/services/graphql/comments.ts | 144 | ||||
| -rw-r--r-- | src/ts/types/app.ts | 4 | ||||
| -rw-r--r-- | src/ts/types/graphql/queries.ts | 4 | ||||
| -rw-r--r-- | src/ts/types/raw-data.ts | 6 | ||||
| -rw-r--r-- | src/utils/hooks/use-comments.tsx | 27 |
10 files changed, 159 insertions, 91 deletions
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx index f62f95c..497a04c 100644 --- a/src/components/organisms/layout/comment.tsx +++ b/src/components/organisms/layout/comment.tsx @@ -1,7 +1,7 @@ import Button from '@components/atoms/buttons/button'; import Link from '@components/atoms/links/link'; import Meta from '@components/molecules/layout/meta'; -import { type Comment as CommentType } from '@ts/types/app'; +import { type SingleComment } from '@ts/types/app'; import useSettings from '@utils/hooks/use-settings'; import Image from 'next/image'; import Script from 'next/script'; @@ -12,7 +12,7 @@ import CommentForm, { type CommentFormProps } from '../forms/comment-form'; import styles from './comment.module.scss'; export type CommentProps = Pick< - CommentType, + SingleComment, 'approved' | 'content' | 'id' | 'meta' | 'parentId' > & Pick<CommentFormProps, 'Notice' | 'saveComment'> & { diff --git a/src/components/organisms/layout/comments-list.fixture.tsx b/src/components/organisms/layout/comments-list.fixture.tsx index 2618f77..f2a1d26 100644 --- a/src/components/organisms/layout/comments-list.fixture.tsx +++ b/src/components/organisms/layout/comments-list.fixture.tsx @@ -1,6 +1,6 @@ -import { Comment } from '@ts/types/app'; +import { SingleComment } from '@ts/types/app'; -export const comments: Comment[] = [ +export const comments: SingleComment[] = [ { approved: true, content: diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx index 97eccb7..deb0776 100644 --- a/src/components/organisms/layout/comments-list.tsx +++ b/src/components/organisms/layout/comments-list.tsx @@ -1,7 +1,7 @@ -import SingleComment, { +import Comment, { type CommentProps, } from '@components/organisms/layout/comment'; -import { Comment } from '@ts/types/app'; +import { SingleComment } from '@ts/types/app'; import { FC } from 'react'; import styles from './comments-list.module.scss'; @@ -9,7 +9,7 @@ export type CommentsListProps = Pick<CommentProps, 'Notice' | 'saveComment'> & { /** * An array of comments. */ - comments: Comment[]; + comments: SingleComment[]; /** * The maximum depth. Use `0` to not display nested comments. */ @@ -30,18 +30,18 @@ const CommentsList: FC<CommentsListProps> = ({ /** * Get each comment wrapped in a list item. * - * @param {Comment[]} commentsList - An array of comments. + * @param {SingleComment[]} commentsList - An array of comments. * @returns {JSX.Element[]} The list items. */ const getItems = ( - commentsList: Comment[], + commentsList: SingleComment[], startLevel: number ): JSX.Element[] => { const isLastLevel = startLevel === depth; return commentsList.map(({ replies, ...comment }) => ( <li key={comment.id} className={styles.item}> - <SingleComment + <Comment canReply={!isLastLevel} Notice={Notice} saveComment={saveComment} diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index 5036b5b..64610b4 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -11,12 +11,12 @@ import { getAllArticlesSlugs, getArticleBySlug, } from '@services/graphql/articles'; -import { getPostComments } from '@services/graphql/comments'; +import { getAllComments } from '@services/graphql/comments'; import styles from '@styles/pages/article.module.scss'; import { type Article, - type Comment, type NextPageWithLayout, + type SingleComment, } from '@ts/types/app'; import { loadTranslation, type Messages } from '@utils/helpers/i18n'; import { @@ -40,7 +40,7 @@ import { HTMLAttributes } from 'react'; import { useIntl } from 'react-intl'; type ArticlePageProps = { - comments: Comment[]; + comments: SingleComment[]; post: Article; slug: string; translation: Messages; @@ -239,7 +239,7 @@ export const getStaticProps: GetStaticProps<ArticlePageProps> = async ({ params, }) => { const post = await getArticleBySlug(params!.slug as PostParams['slug']); - const comments = await getPostComments(post.id as number); + const comments = await getAllComments({ contentId: post.id as number }); const translation = await loadTranslation(locale); return { diff --git a/src/services/graphql/comments.query.ts b/src/services/graphql/comments.query.ts index ef93e89..5110db3 100644 --- a/src/services/graphql/comments.query.ts +++ b/src/services/graphql/comments.query.ts @@ -1,21 +1,32 @@ /** * Query the comments data by post id. */ -export const commentsQuery = `query CommentsByPostId($contentId: ID!) { - comments(where: {contentId: $contentId, order: ASC, orderby: COMMENT_DATE}) { - nodes { - approved - author { - node { - gravatarUrl - name - url +export const commentsQuery = `query CommentsByPostId($contentId: ID!, $first: Int = 10, $after: String = "") { + comments( + where: {contentId: $contentId} + first: $first + after: $after + ) { + edges { + cursor + node { + approved + author { + node { + gravatarUrl + name + url + } } + content + databaseId + date + parentDatabaseId } - content - databaseId - date - parentDatabaseId + } + pageInfo { + hasNextPage + endCursor } } }`; diff --git a/src/services/graphql/comments.ts b/src/services/graphql/comments.ts index 86b6a35..41f80b3 100644 --- a/src/services/graphql/comments.ts +++ b/src/services/graphql/comments.ts @@ -1,46 +1,61 @@ -import { Comment } from '@ts/types/app'; +import { SingleComment } from '@ts/types/app'; import { GraphQLEdgesInput } from '@ts/types/graphql/generics'; import { SendCommentInput, SentComment } from '@ts/types/graphql/mutations'; import { ContentId } from '@ts/types/graphql/queries'; -import { RawComment } from '@ts/types/raw-data'; +import { RawComment, RawCommentsPage } from '@ts/types/raw-data'; import { getAuthorFromRawData } from '@utils/helpers/author'; import { fetchAPI } from './api'; import { sendCommentMutation } from './comments.mutation'; import { commentsQuery } from './comments.query'; -type FetchCommentsInput = ContentId & - Pick<GraphQLEdgesInput, 'after' | 'first'>; - /** - * Retrieve the comments list from GraphQL. + * Convert a comment from RawComment type to SingleComment type. * - * @param {FetchCommentsInput} variables - An object of variables. - * @returns {Promise<RawComment[]>} The raw comments. + * @param {RawComment} comment - A raw comment. + * @returns {SingleComment} A formatted comment. */ -export const fetchComments = async ( - variables: FetchCommentsInput -): Promise<RawComment[]> => { - const response = await fetchAPI<RawComment, typeof commentsQuery>({ - query: commentsQuery, - variables, - }); +export const getCommentFromRawData = (comment: RawComment): SingleComment => { + const { author, databaseId, date, parentDatabaseId, ...data } = comment; - return response.comments.nodes; + return { + id: databaseId, + meta: { + author: getAuthorFromRawData(author.node, 'comment'), + date, + }, + parentId: parentDatabaseId === 0 ? undefined : parentDatabaseId, + replies: [], + ...data, + }; +}; + +/** + * Convert an array of RawComment type to an array of SingleComment type. + * + * @param {RawComment[]} comments - The raw comments. + * @returns {SingleComment[]} The formatted comments. + */ +export const getCommentsFromRawData = ( + comments: RawComment[] +): SingleComment[] => { + return comments.map((comment) => getCommentFromRawData(comment)); }; /** * Create a comments tree with replies. * - * @param {Comment[]} comments - A flatten comments list. - * @returns {Comment[]} An array of comments with replies. + * @param {SingleComment[]} comments - A flatten comments list. + * @returns {SingleComment[]} An array of comments with replies. */ -export const buildCommentsTree = (comments: Comment[]): Comment[] => { +export const buildCommentsTree = ( + comments: SingleComment[] +): SingleComment[] => { type CommentsHashTable = { - [key: string]: Comment; + [key: string]: SingleComment; }; const hashTable: CommentsHashTable = Object.create(null); - const commentsTree: Comment[] = []; + const commentsTree: SingleComment[] = []; comments.forEach( (comment) => (hashTable[comment.id] = { ...comment, replies: [] }) @@ -57,36 +72,85 @@ export const buildCommentsTree = (comments: Comment[]): Comment[] => { return commentsTree; }; +type FetchCommentsInput = ContentId & + Pick<GraphQLEdgesInput, 'after' | 'first'>; + /** - * Convert a comment from RawComment to Comment type. + * Retrieve a raw comments page from GraphQL. * - * @param {RawComment} comment - A raw comment. - * @returns {Comment} A formatted comment. + * @param {FetchCommentsInput} variables - An object of variables. + * @returns {Promise<RawCommentsPage>} A raw comments page. */ -export const getCommentFromRawData = (comment: RawComment): Comment => { - const { author, databaseId, date, parentDatabaseId, ...data } = comment; +export const fetchRawComments = async ( + variables: FetchCommentsInput +): Promise<RawCommentsPage> => { + const response = await fetchAPI<RawComment, typeof commentsQuery>({ + query: commentsQuery, + variables, + }); return { - id: databaseId, - meta: { - author: getAuthorFromRawData(author.node, 'comment'), - date, - }, - parentId: parentDatabaseId, - replies: [], - ...data, + comments: response.comments.edges.map((edge) => edge.node), + hasNextPage: response.comments.pageInfo.hasNextPage, + endCursor: response.comments.pageInfo.endCursor, }; }; /** - * Retrieve a comments list by post id. + * Fetch recursively all the comments on a post. + * + * @param {FetchCommentsInput} variables - An object of query variables. + * @param {RawCommentsPage[]} pages - An accumulator to keep track of pages. + * @returns {Promise<RawCommentsPage[]>} The raw comments pages. + */ +export const fetchAllRawCommentsPages = async ( + variables: FetchCommentsInput, + pages: RawCommentsPage[] = [] +): Promise<RawCommentsPage[]> => { + return fetchRawComments(variables).then((page) => { + pages.push(page); + + if (page.hasNextPage) { + return fetchAllRawCommentsPages( + { ...variables, after: page.endCursor }, + pages + ); + } else { + return pages; + } + }); +}; + +/** + * Method to compare two comments dates and sort them from older to newest. + * + * @param {SingleComment} a - A comment. + * @param {SingleComment} b - Another comment. + * @returns {number} The difference between dates. + */ +export const compareCommentsDate = ( + a: SingleComment, + b: SingleComment +): number => { + return +new Date(a.meta.date) - +new Date(b.meta.date); +}; + +/** + * Retrieve all the comments on a post. * * @param {number} id - A post id. - * @returns {Promise<Comment[]>} The comments list. + * @returns {Promise<SingleComment[]>} The comments list. */ -export const getPostComments = async (id: number): Promise<Comment[]> => { - const rawComments = await fetchComments({ contentId: id }); - const comments = rawComments.map((comment) => getCommentFromRawData(comment)); +export const getAllComments = async ({ + contentId, +}: { + contentId: number; +}): Promise<SingleComment[]> => { + const pages = await fetchAllRawCommentsPages({ contentId }); + const comments = pages + .map((page) => getCommentsFromRawData(page.comments)) + .flat() + .sort(compareCommentsDate); return buildCommentsTree(comments); }; @@ -95,7 +159,7 @@ export const getPostComments = async (id: number): Promise<Comment[]> => { * Send a comment using GraphQL API. * * @param {SendCommentVars} data - The comment data. - * @returns {Promise<SentEmail>} The mutation response. + * @returns {Promise<SentComment>} The mutation response. */ export const sendComment = async ( data: SendCommentInput diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts index 7bf1541..c11c31b 100644 --- a/src/ts/types/app.ts +++ b/src/ts/types/app.ts @@ -39,13 +39,13 @@ export type CommentMeta = { date: string; }; -export type Comment = { +export type SingleComment = { approved: boolean; content: string; id: number; meta: CommentMeta; parentId?: number; - replies: Comment[]; + replies: SingleComment[]; }; export type Dates = { diff --git a/src/ts/types/graphql/queries.ts b/src/ts/types/graphql/queries.ts index cc7b62b..c29eeb3 100644 --- a/src/ts/types/graphql/queries.ts +++ b/src/ts/types/graphql/queries.ts @@ -96,7 +96,7 @@ export type QueriesResponseMap<T> = { [articlesEndCursorQuery]: ArticlesResponse<EndCursorResponse>; [articlesQuery]: ArticlesResponse<EdgesResponse<T>>; [articlesSlugQuery]: ArticlesResponse<EdgesResponse<T>>; - [commentsQuery]: CommentsResponse<GraphQLNodes<T>>; + [commentsQuery]: CommentsResponse<EdgesResponse<T>>; [thematicBySlugQuery]: ThematicResponse<T>; [thematicsListQuery]: ThematicsResponse<EdgesResponse<T>>; [thematicsSlugQuery]: ThematicsResponse<EdgesResponse<T>>; @@ -128,7 +128,7 @@ export type QueriesInputMap = { [articlesEndCursorQuery]: QueryEdges & Search; [articlesQuery]: QueryEdges & Search; [articlesSlugQuery]: QueryEdges & Search; - [commentsQuery]: ContentId; + [commentsQuery]: ContentId & QueryEdges; [thematicBySlugQuery]: Slug; [thematicsListQuery]: QueryEdges & Search; [thematicsSlugQuery]: QueryEdges & Search; diff --git a/src/ts/types/raw-data.ts b/src/ts/types/raw-data.ts index ae7f7c6..022016e 100644 --- a/src/ts/types/raw-data.ts +++ b/src/ts/types/raw-data.ts @@ -44,6 +44,12 @@ export type RawComment = { parentDatabaseId: number; }; +export type RawCommentsPage = { + comments: RawComment[]; + hasNextPage: boolean; + endCursor: string; +}; + export type RawCover = { altText: string; mediaDetails: { diff --git a/src/utils/hooks/use-comments.tsx b/src/utils/hooks/use-comments.tsx index cb0848b..a695bd7 100644 --- a/src/utils/hooks/use-comments.tsx +++ b/src/utils/hooks/use-comments.tsx @@ -1,38 +1,25 @@ -import { fetchAPI } from '@services/graphql/api'; -import { - buildCommentsTree, - getCommentFromRawData, -} from '@services/graphql/comments'; -import { commentsQuery } from '@services/graphql/comments.query'; -import { Comment } from '@ts/types/app'; -import { RawComment } from '@ts/types/raw-data'; +import { getAllComments } from '@services/graphql/comments'; +import { SingleComment } from '@ts/types/app'; import useSWR from 'swr'; export type UseCommentsConfig = { contentId?: string | number; - fallback?: Comment[]; + fallback?: SingleComment[]; }; /** * Retrieve the comments of a page/article. * * @param {string | number} contentId - A page/article id. - * @returns {Comment[]|undefined} + * @returns {SingleComment[]|undefined} */ const useComments = ({ contentId, fallback, -}: UseCommentsConfig): Comment[] | undefined => { - const { data } = useSWR( - contentId ? { query: commentsQuery, variables: { contentId } } : null, - fetchAPI<RawComment, typeof commentsQuery> - ); +}: UseCommentsConfig): SingleComment[] | undefined => { + const { data } = useSWR(contentId ? { contentId } : null, getAllComments); - const comments = data?.comments.nodes.map((comment) => - getCommentFromRawData(comment) - ); - - return comments ? buildCommentsTree(comments) : fallback; + return data || fallback; }; export default useComments; |
