From ab5357c827e150fa0a8875654ab14bb152c76639 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 11 Jan 2025 00:38:55 +0100 Subject: [PATCH] feat: comments --- .../20250110222317_add_comments/migration.sql | 18 ++++++ .../migration.sql | 9 +++ prisma/schema.prisma | 15 +++++ src/app/(public)/post/[id]/page.tsx | 22 ++++--- src/components/app/Comments/Comments.tsx | 54 ++++++++++++++++ .../app/UniversalForm/UniversalForm.tsx | 15 ++++- src/components/app/UniversalForm/types.ts | 2 + .../app/UpvoteComment/UpvoteComment.tsx | 31 +++++++++ src/components/ui/button.tsx | 3 +- src/instrumentation.ts | 1 + src/lib/form/actions.ts | 64 ++++++++++++++++++- src/lib/form/zod.ts | 5 ++ 12 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 prisma/migrations/20250110222317_add_comments/migration.sql create mode 100644 prisma/migrations/20250110231356_improve_comments/migration.sql create mode 100644 src/components/app/Comments/Comments.tsx create mode 100644 src/components/app/UpvoteComment/UpvoteComment.tsx diff --git a/prisma/migrations/20250110222317_add_comments/migration.sql b/prisma/migrations/20250110222317_add_comments/migration.sql new file mode 100644 index 0000000..73c1d24 --- /dev/null +++ b/prisma/migrations/20250110222317_add_comments/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "Comment" ( + "id" TEXT NOT NULL, + "postId" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "upvotes" INTEGER NOT NULL, + "downvotes" INTEGER NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250110231356_improve_comments/migration.sql b/prisma/migrations/20250110231356_improve_comments/migration.sql new file mode 100644 index 0000000..95d5b7b --- /dev/null +++ b/prisma/migrations/20250110231356_improve_comments/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `downvotes` on the `Comment` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Comment" DROP COLUMN "downvotes", +ADD COLUMN "votedBy" TEXT[]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4b71549..c1d773e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { likedPosts String[] sessions Session[] posts Post[] + comments Comment[] } model Session { @@ -38,4 +39,18 @@ model Post { authorId String author User @relation(references: [id], fields: [authorId], onDelete: Cascade) previewHash String + + comments Comment[] } + +model Comment { + id String @id @default(cuid()) + postId String + authorId String + content String + createdAt DateTime @default(now()) + upvotes Int + votedBy String[] + post Post @relation(references: [id], fields: [postId], onDelete: Cascade) + author User @relation(references: [id], fields: [authorId], onDelete: Cascade) +} \ No newline at end of file diff --git a/src/app/(public)/post/[id]/page.tsx b/src/app/(public)/post/[id]/page.tsx index 4fd0cca..72586d0 100644 --- a/src/app/(public)/post/[id]/page.tsx +++ b/src/app/(public)/post/[id]/page.tsx @@ -1,3 +1,4 @@ +import Comments from '@/components/app/Comments/Comments'; import LikePost from '@/components/app/LikePost/LikePost'; import RelatedImages, { RelatedImagesSkeleton } from '@/components/app/RelatedImages/RelatedImages'; import { Badge } from '@/components/ui/badge'; @@ -8,6 +9,7 @@ import { validateRequest } from '@/lib/auth'; import prisma from '@/lib/db'; import { Download, Flag, MessageSquare, User, Calendar } from 'lucide-react'; import Image from 'next/image'; +import Link from 'next/link'; import { Suspense } from 'react'; export default async function Page({ params }: { params: Promise<{ id: string }> }) { @@ -46,14 +48,18 @@ export default async function Page({ params }: { params: Promise<{ id: string }> - + + + -

comments here

+ Loading comments...}> + +

Image information

@@ -75,11 +81,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>

Tags

{post.tags.map((tag) => ( - + {tag} ))} diff --git a/src/components/app/Comments/Comments.tsx b/src/components/app/Comments/Comments.tsx new file mode 100644 index 0000000..82a8a32 --- /dev/null +++ b/src/components/app/Comments/Comments.tsx @@ -0,0 +1,54 @@ +import prisma from '@/lib/db'; +import { UniversalForm } from '../UniversalForm/UniversalForm'; +import { createComment, upvoteComment } from '@/lib/form/actions'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { ArrowUp } from 'lucide-react'; +import UpvoteComment from '../UpvoteComment/UpvoteComment'; +import { validateRequest } from '@/lib/auth'; + +export default async function Comments(props: Props) { + const { user } = await validateRequest(); + const comments = await prisma.comment.findMany({ + where: { postId: props.id }, + orderBy: { createdAt: 'asc' }, + include: { author: true }, + }); + + return ( +
+
+ {comments.map((comment) => ( +
+ + {comment.author.username[0]} + +
+

{comment.author.username}

+

{comment.content}

+
+
+
+

{comment.upvotes}

+ +
+
+ ))} +
+ +
+ ); +} + +interface Props { + id: string; +} diff --git a/src/components/app/UniversalForm/UniversalForm.tsx b/src/components/app/UniversalForm/UniversalForm.tsx index 250cf07..8a0da9f 100644 --- a/src/components/app/UniversalForm/UniversalForm.tsx +++ b/src/components/app/UniversalForm/UniversalForm.tsx @@ -19,12 +19,13 @@ import React from 'react'; import { toast } from 'sonner'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; -import { accountSchema, uploadSchema } from '@/lib/form/zod'; +import { accountSchema, commentSchema, uploadSchema } from '@/lib/form/zod'; export const schemaDb = [ { name: 'login', zod: accountSchema }, { name: 'register', zod: accountSchema }, { name: 'upload', zod: uploadSchema }, + { name: 'comment', zod: commentSchema }, ] as const; export function UniversalForm({ @@ -37,10 +38,12 @@ export function UniversalForm({ submitClassname, otherSubmitButton, submitButtonDivClassname, + resetFormOnSubmit, }: UniversalFormProps) { // @ts-ignore idk why this error is happening, first apprearing on the react 19 update. const [state, formAction] = useActionState<{ success: boolean; error?: string }>(action, null); const schema = schemaDb.find((s) => s.name === schemaName)?.zod; + const formRef = React.useRef(null); if (!schema) { throw new Error(`Schema "${schemaName}" not found`); @@ -60,6 +63,12 @@ export function UniversalForm({ defaultValues: initialValues as z.infer, }); + React.useEffect(() => { + if (state?.success && resetFormOnSubmit) { + form.reset(initialValues as z.infer); + } + }, [state, resetFormOnSubmit, form, initialValues]); + React.useEffect(() => { if (state && !state.success) { toast.error(state.error); @@ -71,7 +80,7 @@ export function UniversalForm({ return (
- + {fields.map((field) => ( ({ {...formField} value={formField.value ?? ''} rows={field.textAreaRows ?? 5} + required={field.required} /> ) : ( ({ placeholder={field.placeholder} {...formField} value={formField.value ?? ''} + required={field.required} /> )} {field.customComponent && field.customComponent} diff --git a/src/components/app/UniversalForm/types.ts b/src/components/app/UniversalForm/types.ts index 111670e..dc90857 100644 --- a/src/components/app/UniversalForm/types.ts +++ b/src/components/app/UniversalForm/types.ts @@ -13,6 +13,7 @@ export type FormFieldConfig = { value?: string; textArea?: boolean; textAreaRows?: number; + required?: boolean; }; export type UniversalFormProps = { @@ -25,4 +26,5 @@ export type UniversalFormProps = { submitClassname?: string; otherSubmitButton?: React.ReactNode; submitButtonDivClassname?: string; + resetFormOnSubmit?: boolean; }; \ No newline at end of file diff --git a/src/components/app/UpvoteComment/UpvoteComment.tsx b/src/components/app/UpvoteComment/UpvoteComment.tsx new file mode 100644 index 0000000..bbe360d --- /dev/null +++ b/src/components/app/UpvoteComment/UpvoteComment.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { upvoteComment } from '@/lib/form/actions'; +import { cn } from '@/lib/utils'; +import type { Comment } from '@prisma/client'; +import { ArrowUp } from 'lucide-react'; +import { useState } from 'react'; + +export default function UpvoteComment(comment: Comment & { userVoted: boolean }) { + const [loading, setLoading] = useState(false); + const [voted, setVoted] = useState(comment.userVoted); + return ( + + ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index e4bd357..f6cfee2 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -25,7 +25,8 @@ const buttonVariants = cva( default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", - icon: "h-10 w-10" + icon: "h-10 w-10", + smicon: "h-8 w-8", }, }, defaultVariants: { diff --git a/src/instrumentation.ts b/src/instrumentation.ts index cc88a19..445f97a 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -83,6 +83,7 @@ export async function register() { } } + await ephemeralStorage.clear('tag') for (const [tag, count] of Object.entries(occurrences)) { await ephemeralStorage.set(`tag:${tag}`, count); } diff --git a/src/lib/form/actions.ts b/src/lib/form/actions.ts index c34e54d..b8b2055 100644 --- a/src/lib/form/actions.ts +++ b/src/lib/form/actions.ts @@ -1,11 +1,12 @@ 'use server'; import zodVerify from '../zodVerify'; -import { uploadSchema } from './zod'; +import { commentSchema, uploadSchema } from './zod'; import prisma from '../db'; import { validateRequest } from '../auth'; import hashImage from '../hashImage'; import minio from '../services/minio'; +import { revalidatePath } from 'next/cache'; export async function create(prev: any, formData: FormData) { const { user } = await validateRequest(); @@ -45,3 +46,64 @@ export async function create(prev: any, formData: FormData) { }); return { success: true, id: dbCreate.id }; } + +export async function createComment(prev: any, formData: FormData) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, message: 'not logged in' }; + } + const zod = await zodVerify(commentSchema, formData); + if (!zod.success) { + return zod; + } + + await prisma.comment.create({ + data: { + content: zod.data.content, + author: { + connect: { + id: user.id, + }, + }, + post: { + connect: { + id: zod.data.postId, + }, + }, + upvotes: 0, + }, + }); + revalidatePath(`/post/${zod.data.postId}`); + return { success: true }; +} + +export async function upvoteComment(commentId: string) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, message: 'not logged in' }; + } + + const comment = await prisma.comment.findUnique({ where: { id: commentId } }); + if (!comment) { + return { success: false, message: 'comment not found' }; + } + + if (comment.votedBy.includes(user.id)) { + await prisma.comment.update({ + where: { id: commentId }, + data: { + upvotes: comment.upvotes - 1, + votedBy: { set: comment.votedBy.filter((id) => id !== user.id) }, + }, + }); + revalidatePath(`/post/${comment.postId}`); + return { success: true, action: 'down' }; + } else { + await prisma.comment.update({ + where: { id: commentId }, + data: { upvotes: comment.upvotes + 1, votedBy: { push: user.id } }, + }); + revalidatePath(`/post/${comment.postId}`); + return { success: true, action: 'up' }; + } +} diff --git a/src/lib/form/zod.ts b/src/lib/form/zod.ts index fa505c0..3c6c22c 100644 --- a/src/lib/form/zod.ts +++ b/src/lib/form/zod.ts @@ -20,3 +20,8 @@ export const uploadSchema = z.object({ caption: z.string().min(1), tags: z.string().min(1), }); + +export const commentSchema = z.object({ + postId: z.string().min(1), + content: z.string().min(1), +}); \ No newline at end of file