Skip to content

Commit

Permalink
feat: comment reply
Browse files Browse the repository at this point in the history
  • Loading branch information
steveschult committed Nov 19, 2024
1 parent 2832162 commit 411e110
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 35 deletions.
2 changes: 2 additions & 0 deletions app/(blog)/posts/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export default async function Page({ params }: { params: { slug: string[] } }) {
const { PostDetail } = await loadTheme()
if (!PostDetail) throw new Error('Missing PostDetail component')

// console.log('=====post:', post)

/** No gated */
if (post?.gateType == GateType.FREE) {
return (
Expand Down
91 changes: 89 additions & 2 deletions components/PostActions/Comment/CommentContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ import LoadingDots from '@/components/icons/loading-dots'
import { UserAvatar } from '@/components/UserAvatar'
import { trpc } from '@/lib/trpc'
import { CommentInput } from './CommentInput'
import { useState } from 'react'
import { User } from '@prisma/client'

interface IParent extends Comment {
user: User
}

interface IReply {
id: string;
content: string;
createdAt: string;
updatedAt: string;
userId: string;
user: User;
parent?: IParent;
parentId: string;
postId: string;
}

interface Props {
postId: string
Expand All @@ -16,6 +34,21 @@ export function CommentContent({ postId }: Props) {
refetch,
} = trpc.comment.listByPostId.useQuery(postId)

const { isPending, mutateAsync: listRepliesByCommentId } = trpc.comment.listRepliesByCommentId.useMutation()
const [showReplyInput, setShowReplyInput] = useState<string>('');
const [showReplies, setShowReplies] = useState<string>('');
const [replies, setReplies] = useState<IReply[]>([]);

const onReplies = async (commentId: string) => {
try {
const data = await listRepliesByCommentId(commentId);
setShowReplies(commentId)
setReplies(data as unknown as IReply[]);
} catch (error) {
console.error('Error fetching replies:', error);
}
}

return (
<div className="flex-col">
<div>
Expand All @@ -34,11 +67,65 @@ export function CommentContent({ postId }: Props) {
className="h-8 w-8"
/>
<p className="ml-1 text-sm text-gray-600">
{' '}
{comment.user?.address}{' '}
{comment.user?.address}
</p>
</div>
<p className="mt-2 ml-1">{comment.content}</p>
<div className="flex justify-between mb-1 ml-1">
<button className="cursor-pointer text-xs hover:underline" onClick={() => onReplies(comment.id)}>
1 reply
</button>
<button className="cursor-pointer text-xs hover:underline" onClick={() => setShowReplyInput(comment.id)}>
Reply
</button>
</div>

{showReplyInput === comment.id && (
<CommentInput postId={comment.postId} refetchComments={refetch} parentId={comment.id} onCancel={() => {
setShowReplyInput('')
}}
/>
)}

{(replies.length > 0 && showReplies === comment.id) && (
<div className="ml-6 mt-4 border-l border-gray-200 pl-4">
{replies.map((reply) => (
<div key={reply.id} className="mb-3">
<div className="flex items-center mb-1">
<UserAvatar
address={reply.user.address as string}
className="h-6 w-6"
/>
<p className="ml-2 text-sm text-gray-600 font-bold">
{reply.user?.address ? `${reply.user.address.slice(0, 10)}...` : ''}
</p>
{reply.parent?.user && (
<p className="ml-2 text-sm text-gray-400">
replied to{' '}
<span className="font-bold text-gray-500">
{reply.user?.address ? `${reply.user.address.slice(0, 10)}...` : ''}
</span>
</p>
)}
</div>
<p className="ml-2 text-gray-700">{reply.content}</p>

<div className="flex justify-end">
<button className="cursor-pointer text-xs hover:underline" onClick={() => setShowReplyInput(reply.id)}>
Reply
</button>
</div>

{showReplyInput === reply.id && (
<CommentInput postId={comment.postId} refetchComments={refetch} parentId={reply.id} onCancel={() => {
setShowReplyInput('')
}}
/>
)}
</div>
))}
</div>
)}
</div>
))
) : (
Expand Down
53 changes: 35 additions & 18 deletions components/PostActions/Comment/CommentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Textarea } from '@/components/ui/textarea'
import { WalletConnectButton } from '@/components/WalletConnectButton'
import { trpc } from '@/lib/trpc'
import { useSession } from 'next-auth/react'
import { revalidatePath } from 'next/cache'
import { toast } from 'sonner'
import { z } from 'zod'

Expand All @@ -19,12 +18,15 @@ const CommentSchema = z.object({

interface Props {
postId: string
// For reply
parentId?: string;
refetchComments: () => void
onCancel?: () => void
}

const maxCharacters = 1000

export function CommentInput({ postId, refetchComments }: Props) {
export function CommentInput({ postId, parentId, refetchComments, onCancel }: Props) {
const userID = useAddress()
const [content, setContent] = useState('')
const { isPending, mutateAsync } = trpc.comment.create.useMutation()
Expand All @@ -50,6 +52,7 @@ export function CommentInput({ postId, refetchComments }: Props) {
postId,
userId: userID as string,
content,
parentId
})

setContent('')
Expand All @@ -62,29 +65,43 @@ export function CommentInput({ postId, refetchComments }: Props) {
}

return (
<div className="space-y-4">
<div>
<Textarea
placeholder="Write your comment..."
value={content}
onChange={(e) => setContent(e.target.value)}
maxLength={maxCharacters}
className="w-full"
/>
<div className="text-sm text-gray-500 flex justify-between">
<span>
{content.length}/{maxCharacters} characters
</span>
</div>
<div className="flex justify-end">
{!authenticated ? (
<WalletConnectButton className="w-30">
Log in to comment
</WalletConnectButton>
) : (
<Button onClick={handleSubmit} className="w-25">
{isPending ? <LoadingDots /> : <p>Comment</p>}
</Button>
)}
<div className="flex items-center justify-between text-sm mt-2 text-gray-500">
<div>
<span>
{content.length}/{maxCharacters} characters
</span>
</div>

<div className="flex justify-end">
{
parentId && <Button
onClick={() => {
setContent('')
onCancel && onCancel()
}}
className="w-20 text-xs h-8 mr-1">
<p>Cancel</p>
</Button>
}

{!authenticated ? (
<WalletConnectButton className="w-30 text-xs h-8">
Log in to comment
</WalletConnectButton>
) : (
<Button onClick={handleSubmit} className="w-20 text-xs h-8">
{isPending ? <LoadingDots /> : <p>Comment</p>}
</Button>
)}
</div>
</div>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion components/PostActions/Comment/CommentSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function CommentSheet({ post }: Props) {
<>
<CommentAmount post={post as any} setIsOpen={setIsOpen} />
<Sheet open={isOpen} onOpenChange={(v) => setIsOpen(v)}>
<SheetContent className="flex flex-col gap-6">
<SheetContent className="flex flex-col gap-6 overflow-y-auto">
<SheetHeader>
<SheetTitle>Comments</SheetTitle>
</SheetHeader>
Expand Down
9 changes: 7 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,20 @@ model Post {
model Comment {
id String @id @default(uuid()) @db.Uuid
content String @default("") @db.Text
parentId String? @db.Uuid
// Points to the ID of the parent comment, null for the root comment
parentId String? @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
post Post @relation(fields: [postId], references: [id])
postId String @db.Uuid
user User @relation(fields: [userId], references: [id])
userId String @db.Uuid
replyCount Int @default(0) @db.Integer
@@index([userId, postId])
parent Comment? @relation("ReplyRelation", fields: [parentId], references: [id]) // parent comments
replies Comment[] @relation("ReplyRelation") // sub comments
@@index([parentId, postId])
@@index([postId])
@@index([userId])
@@map("comment")
Expand Down
52 changes: 40 additions & 12 deletions server/routers/comment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { prisma } from '@/lib/prisma'
import { revalidateMetadata } from '@/lib/revalidateTag'
import { revalidatePath, revalidateTag } from 'next/cache'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
import { protectedProcedure, publicProcedure, router } from '../trpc'

Expand All @@ -10,7 +9,7 @@ export const commentRouter = router({
.input(z.string())
.query(async ({ ctx, input: postId }) => {
const comments = await prisma.comment.findMany({
where: { postId },
where: { postId, parentId: null }, // Only top-level comments (parentId === null) need to be queried
include: {
user: true, // Assuming you want to include user details in comments
},
Expand Down Expand Up @@ -39,20 +38,49 @@ export const commentRouter = router({
},
})

const updatedPost = await prisma.post.update({
where: { id: input.postId },
data: {
commentCount: { increment: 1 },
},
})
if (input.parentId) {
const updatedComment = await prisma.comment.update({
where: { id: input.parentId },
data: {
replyCount: { increment: 1 },
},
})
} else {
const updatedPost = await prisma.post.update({
where: { id: input.postId },
data: {
commentCount: { increment: 1 },
},
})

revalidatePath('/(blog)/(home)', 'page')
revalidatePath('/(blog)/posts', 'page')
revalidatePath(`/posts/${updatedPost.slug}`)
revalidatePath('/(blog)/(home)', 'page')
revalidatePath('/(blog)/posts', 'page')
revalidatePath(`/posts/${updatedPost.slug}`)
}

return newComment
}),

listRepliesByCommentId: publicProcedure
.input(z.string())
.mutation(async ({ ctx, input: commentId }) => {
const replies = await prisma.comment.findMany({
where: { parentId: commentId },
include: {
user: true,
parent: {
include: {
user: true,
},
},
},
orderBy: { createdAt: 'asc' },
});

return replies;
}),


// Update an existing comment
update: protectedProcedure
.input(
Expand Down

0 comments on commit 411e110

Please sign in to comment.