diff options
| -rw-r--r-- | src/components/Comment/Comment.module.scss | 87 | ||||
| -rw-r--r-- | src/components/Comment/Comment.tsx | 88 | ||||
| -rw-r--r-- | src/components/CommentsList/CommentsList.module.scss | 5 | ||||
| -rw-r--r-- | src/components/CommentsList/CommentsList.tsx | 24 | ||||
| -rw-r--r-- | src/pages/article/[slug].tsx | 6 | ||||
| -rw-r--r-- | src/services/graphql/post.ts | 5 | ||||
| -rw-r--r-- | src/ts/types/comments.ts | 12 |
7 files changed, 225 insertions, 2 deletions
diff --git a/src/components/Comment/Comment.module.scss b/src/components/Comment/Comment.module.scss new file mode 100644 index 0000000..8f17bfa --- /dev/null +++ b/src/components/Comment/Comment.module.scss @@ -0,0 +1,87 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.item { + margin: var(--spacing-sm) 0; +} + +.wrapper { + border: fun.convert-px(1) solid var(--color-border-light); + box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow-light), + fun.convert-px(4) fun.convert-px(4) fun.convert-px(3) fun.convert-px(-2) + var(--color-shadow-light); + padding: var(--spacing-md); + position: relative; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + display: grid; + grid-template-columns: minmax(#{fun.convert-px(150)}, 1fr) minmax(0, 85ch); + column-gap: var(--spacing-lg); + } + } +} + +.header { + display: flex; + flex-flow: column wrap; + align-items: center; + row-gap: var(--spacing-sm); + + @include mix.media("screen") { + @include mix.dimensions("sm") { + grid-row: 1 / 4; + } + } +} + +.avatar { + width: fun.convert-px(85); + height: fun.convert-px(85); + border-radius: fun.convert-px(3); + box-shadow: 0 0 0 fun.convert-px(1) var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow-light); + position: relative; +} + +.date { + color: var(--color-fg-secondary); + font-size: var(--font-size-sm); + display: grid; + grid-template-columns: repeat(2, max-content); + column-gap: var(--spacing-2xs); + justify-content: center; + margin: var(--spacing-sm) 0 var(--spacing-md); + + @include mix.media("screen") { + @include mix.dimensions("sm") { + justify-content: left; + margin: 0 0 var(--spacing-md); + } + } +} + +.body { + overflow-wrap: break-word; +} + +.footer { + display: flex; + justify-content: flex-end; + align-items: center; + padding: var(--spacing-md) 0 0; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + padding: var(--spacing-sm) 0 0; + } + } + + button { + margin: 0; + } +} + +.list { + padding-left: var(--spacing-md); +} diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx new file mode 100644 index 0000000..ad6875c --- /dev/null +++ b/src/components/Comment/Comment.tsx @@ -0,0 +1,88 @@ +import { Button } from '@components/Buttons'; +import { t } from '@lingui/macro'; +import { Comment as CommentData } from '@ts/types/comments'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import styles from './Comment.module.scss'; + +const Comment = ({ comment }: { comment: CommentData }) => { + const router = useRouter(); + + const getCommentAuthor = () => { + return comment.author.url ? ( + <Link href={comment.author.url}> + <a>{comment.author.name}</a> + </Link> + ) : ( + <span>{comment.author.name}</span> + ); + }; + + const getLocaleDate = () => { + const commentDate = new Date(comment.date); + const date = commentDate.toLocaleDateString(router.locale, { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + const time = commentDate + .toLocaleTimeString(router.locale, { + hour: 'numeric', + minute: 'numeric', + }) + .replace(':', 'h'); + return t`${date} at ${time}`; + }; + + const getApprovedComment = () => { + return ( + <> + <article className={styles.wrapper}> + <header className={styles.header}> + {comment.author.gravatarUrl && ( + <div className={styles.avatar}> + <Image + src={comment.author.gravatarUrl} + alt={comment.author.name} + layout="fill" + /> + </div> + )} + {getCommentAuthor()} + </header> + <dl className={styles.date}> + <dt>{t`Published on:`}</dt> + <dd>{getLocaleDate()}</dd> + </dl> + <div + className={styles.body} + dangerouslySetInnerHTML={{ __html: comment.content }} + ></div> + <footer className={styles.footer}> + <Button clickHandler={() => ''}>{t`Reply`}</Button> + </footer> + </article> + {comment.replies.length > 0 && ( + <ol className={styles.list}> + {comment.replies.map((reply) => { + return <Comment key={reply.id} comment={reply} />; + })} + </ol> + )} + </> + ); + }; + + const getCommentStatus = () => { + return <p>{t`This comment is awaiting moderation.`}</p>; + }; + + return ( + <li className={styles.item}> + {comment.approved ? getApprovedComment() : getCommentStatus()} + </li> + ); +}; + +export default Comment; diff --git a/src/components/CommentsList/CommentsList.module.scss b/src/components/CommentsList/CommentsList.module.scss new file mode 100644 index 0000000..2358948 --- /dev/null +++ b/src/components/CommentsList/CommentsList.module.scss @@ -0,0 +1,5 @@ +@use "@styles/abstracts/placeholders"; + +.list { + @extend %reset-ordered-list; +} diff --git a/src/components/CommentsList/CommentsList.tsx b/src/components/CommentsList/CommentsList.tsx new file mode 100644 index 0000000..6599475 --- /dev/null +++ b/src/components/CommentsList/CommentsList.tsx @@ -0,0 +1,24 @@ +import { Comment as CommentData } from '@ts/types/comments'; +import Comment from '@components/Comment/Comment'; +import { t } from '@lingui/macro'; +import styles from './CommentsList.module.scss'; + +const CommentsList = ({ comments }: { comments: CommentData[] }) => { + const getCommentsList = () => { + return comments.map((comment) => { + return <Comment key={comment.id} comment={comment} />; + }); + }; + + return ( + <> + {comments.length > 0 ? ( + <ol className={styles.list}>{getCommentsList()}</ol> + ) : ( + <p>{t`No comments yet.`}</p> + )} + </> + ); +}; + +export default CommentsList; diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index 7ad3692..b3ab2ed 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -1,4 +1,6 @@ +import CommentsList from '@components/CommentsList/CommentsList'; import Layout from '@components/Layouts/Layout'; +import { t } from '@lingui/macro'; import { fetchAllPostsSlug } from '@services/graphql/blog'; import { getPostBySlug } from '@services/graphql/post'; import { NextPageWithLayout } from '@ts/types/app'; @@ -16,6 +18,10 @@ const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => { <div dangerouslySetInnerHTML={{ __html: post.intro }}></div> </header> <div dangerouslySetInnerHTML={{ __html: post.content }}></div> + <section> + <h2>{t`Comments`}</h2> + <CommentsList comments={post.comments} /> + </section> </article> ); }; diff --git a/src/services/graphql/post.ts b/src/services/graphql/post.ts index 2ce918b..e827fd4 100644 --- a/src/services/graphql/post.ts +++ b/src/services/graphql/post.ts @@ -104,7 +104,10 @@ const fetchPostBySlug: FetchPostByReturn = async (slug: string) => { export const getPostBySlug: GetPostByReturn = async (slug: string) => { const rawPost = await fetchPostBySlug(slug); - const comments = rawPost.postBy.comments.nodes; + const comments = rawPost.postBy.comments.nodes.reverse().map((comment) => { + const author = comment.author.node; + return { ...comment, author: author, replies: [] }; + }); const content = rawPost.postBy.contentParts.afterMore; const featuredImage = rawPost.postBy.featuredImage ? rawPost.postBy.featuredImage.node diff --git a/src/ts/types/comments.ts b/src/ts/types/comments.ts index b196142..51852d0 100644 --- a/src/ts/types/comments.ts +++ b/src/ts/types/comments.ts @@ -4,6 +4,10 @@ export type CommentAuthor = { url: string; }; +export type CommentAuthorResponse = { + node: CommentAuthor; +}; + export type Comment = { approved: ''; author: CommentAuthor; @@ -11,8 +15,14 @@ export type Comment = { content: string; date: string; id: string; + parentDatabaseId: number; + replies: Comment[]; +}; + +export type RawComment = Omit<Comment, 'author'> & { + author: CommentAuthorResponse; }; export type CommentsResponse = { - nodes: Comment[]; + nodes: RawComment[]; }; |
