From e0a116c4f2cbee54becd173be92d4cd320e69870 Mon Sep 17 00:00:00 2001 From: Ryan Freeman Date: Wed, 2 Apr 2025 22:41:37 +0100 Subject: [PATCH] Added comment reply functionality --- app/actions/comments.ts | 22 +- components/icons/ArrowLeftIcon.tsx | 10 + components/ui/Comments.tsx | 150 ++++++++-- lib/getComments.ts | 31 +- package.json | 2 +- types/database.types.ts | 438 ++++++++++++++++------------- 6 files changed, 411 insertions(+), 242 deletions(-) create mode 100644 components/icons/ArrowLeftIcon.tsx diff --git a/app/actions/comments.ts b/app/actions/comments.ts index e8a775a..5033c81 100644 --- a/app/actions/comments.ts +++ b/app/actions/comments.ts @@ -41,21 +41,27 @@ export async function addComment(prevState: { message: string }, formData: FormD const authorisation_error = 'Error, you must be logged in to post a comment.' const success_message = 'Thanks, your comment was submitted and is awaiting approval.' + console.log(formData) + const schema = z.object({ - comment: z.string().min(3).max(255), - slug: z.string() + comment: z.string().min(3).max(255).trim(), + slug: z.string(), + parent_id: z.string().optional() }) - const {comment, slug} = { + let {comment, slug, parent_id} = { comment: formData.get('comment'), - slug: formData.get('slug') + slug: formData.get('slug'), + parent_id: formData.get('parent_id') } - const parse = schema.safeParse({comment, slug}) + const parse = schema.safeParse({comment, slug, parent_id}); if (!parse.success) { return {message: general_error} } + if (parent_id === '') parent_id = null + try { const supabase = await createClient() const session = await auth() @@ -73,7 +79,7 @@ export async function addComment(prevState: { message: string }, formData: FormD const {data: newComment, error} = await supabase .from('comments') - .insert({content: comment, article_id: article?.id, user_id: user?.id}) + .insert({content: comment, article_id: article?.id, user_id: user?.id, parent_id: parent_id}) .select('*') .single() @@ -81,7 +87,9 @@ export async function addComment(prevState: { message: string }, formData: FormD return {message: general_error} } - await sendNotification(notificationBody(newComment, user, article)) + if (process.env.NODE_ENV === 'production') { + await sendNotification(notificationBody(newComment, user, article)) + } return {message: success_message} } catch (error) { diff --git a/components/icons/ArrowLeftIcon.tsx b/components/icons/ArrowLeftIcon.tsx new file mode 100644 index 0000000..dd06c4a --- /dev/null +++ b/components/icons/ArrowLeftIcon.tsx @@ -0,0 +1,10 @@ +import {Props} from '@/types' + +export function ArrowLeftIcon(props: Props) { + return ( + + ) +} \ No newline at end of file diff --git a/components/ui/Comments.tsx b/components/ui/Comments.tsx index 2c8a9ea..f34d274 100644 --- a/components/ui/Comments.tsx +++ b/components/ui/Comments.tsx @@ -1,6 +1,6 @@ 'use client' -import React, {ReactNode, useActionState} from 'react' +import React, {createContext, ReactNode, RefObject, useActionState, useContext, useEffect, useRef, useState} from 'react' import {useSession} from 'next-auth/react' import Image from 'next/image' import clsx from 'clsx' @@ -8,35 +8,76 @@ import {addComment, loginWithGitHub} from '@/app/actions/comments' import {Button} from '@/components/ui/Button' import {GitHubIcon} from '@/components/icons/SocialIcons' import {formatDistanceToNow} from 'date-fns' +import {ArrowLeftIcon} from '@/components/icons/ArrowLeftIcon' type Comment = { id: number content: string created_at: string + parent_id: number | null user: { id: number name: string image: string } + replies?: Comment[] } -Comments.Comment = function Comment({comment, isReply = false}: { comment: Comment, isReply?: boolean }) { +type ReplyButton = { + comment: Comment +} + +Comments.ReplyButton = function ReplyButton({comment}: ReplyButton) { + const replyContext = useContext(ReplyContext) + const commentFormContext = useContext(CommentFormContext) + + const handleReplyButton = (e: React.MouseEvent) => { + e.preventDefault() + replyContext?.setReplyTo(comment) + commentFormContext?.focusCommentForm() + } + return ( -
- {comment.user.name} -
-
-

{comment.user.name}

-

- -

+ + ) +} + +Comments.Comment = function Comment({comment, children, isReply = false}: { + comment: Comment, + children?: ReactNode, + isReply?: boolean +}) { + const {data: session} = useSession() + + return ( + <> +
+ {comment.user.name} +
+
+

{comment.user.name}

+

+ +

+
+

{comment.content}

+ {session && + + }
-

{comment.content}

-
-
+ + {children} + ) } @@ -53,7 +94,11 @@ Comments.List = function List({comments}: CommentsListProps) { Comments {comments.map((comment) => ( - + + {comment.replies && comment.replies.map(reply => ( + + ))} + ))} } @@ -66,8 +111,8 @@ type CommentsStatusProps = { } Comments.Status = function Status({children}: CommentsStatusProps) { - const conditions = ['error', 'problem'] - const isError = conditions.some(condition => children?.toString().toLowerCase().includes(condition)) + const errorConditions = ['error', 'problem'] + const isError = errorConditions.some(condition => children?.toString().toLowerCase().includes(condition)) return (

void } | null>(null) + +Comments.Form = function Form({slug, commentFormRef}: CommentsProps) { + const [parentId, setParentId] = useState('') const [state, formAction, pending] = useActionState(addComment, initialState) const {data: session} = useSession() + const replyContext = useContext(ReplyContext) + + useEffect(() => { + if (replyContext?.replyTo?.parent_id !== null) { + setParentId(replyContext?.replyTo?.parent_id ?? '') + } else { + setParentId(replyContext?.replyTo?.id) + } + }, [replyContext?.replyTo]) const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && (e.key === 'Enter' || e.key === 'NumpadEnter')) { e.preventDefault() e.currentTarget.form?.requestSubmit() } + + if (e.key === 'Escape' && replyContext?.replyTo !== null) { + replyContext?.setReplyTo(null) + commentFormRef?.current?.blur() + } } return ( @@ -108,7 +170,7 @@ Comments.Form = function Form({slug}: CommentsProps) {