From 9545be46d8d2ad0a543ffa479bd5a9649cb0f559 Mon Sep 17 00:00:00 2001 From: "vishalkondle.dev" Date: Tue, 9 Jul 2024 23:24:20 +0530 Subject: [PATCH] feat: forum - upvote | downvote | saved --- app/(private)/forum/[forum]/page.tsx | 17 +- app/api/forums/reply/route.ts | 24 +++ app/api/forums/route.ts | 21 +- components/Forum/Forum.tsx | 285 ++++++++++++++++++--------- components/Forum/ReplyToQuestion.tsx | 50 +++++ components/Forum/index.ts | 1 + models/Forum.ts | 10 +- 7 files changed, 303 insertions(+), 105 deletions(-) create mode 100644 app/api/forums/reply/route.ts create mode 100644 components/Forum/ReplyToQuestion.tsx diff --git a/app/(private)/forum/[forum]/page.tsx b/app/(private)/forum/[forum]/page.tsx index 7ed5089..e6b6627 100644 --- a/app/(private)/forum/[forum]/page.tsx +++ b/app/(private)/forum/[forum]/page.tsx @@ -1,15 +1,16 @@ 'use client'; -import { Button, Group } from '@mantine/core'; +import { Button, Group, Stack } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import { Forum, AskQuestion } from '@/components/Forum'; +import { Forum, AskQuestion, ReplyToQuestion } from '@/components/Forum'; import useFetchData from '@/hooks/useFetchData'; +import { ForumType } from '@/components/Forum/Forum.types'; const ForumPage = ({ params }: { params: { forum: string } }) => { - const { data, loading } = useFetchData(`/api/forums?_id=${params.forum}`); + const { data, refetch } = useFetchData(`/api/forums?_id=${params.forum}`); const [opened, { open, close }] = useDisclosure(false); - if (loading) { + if (!data?.length) { return <>; } @@ -18,7 +19,13 @@ const ForumPage = ({ params }: { params: { forum: string } }) => { - + + + {data?.[1]?.map((forum: ForumType) => ( + + ))} + + ); diff --git a/app/api/forums/reply/route.ts b/app/api/forums/reply/route.ts new file mode 100644 index 0000000..5b5396a --- /dev/null +++ b/app/api/forums/reply/route.ts @@ -0,0 +1,24 @@ +import { getServerSession } from 'next-auth'; +import { NextRequest, NextResponse } from 'next/server'; +import startDb from '@/lib/db'; +import Forum from '@/models/Forum'; +import { authOptions } from '../../auth/[...nextauth]/authOptions'; +import { UserDataTypes } from '../../auth/[...nextauth]/next-auth.interfaces'; + +export const maxDuration = 60; +export const dynamic = 'force-dynamic'; + +export async function POST(req: NextRequest) { + try { + const session: UserDataTypes | null = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'You are not authorized' }, { status: 401 }); + } + const body = await req.json(); + await startDb(); + const forum = await Forum.create({ ...body, user: session?.user._id }); + return NextResponse.json(forum, { status: 200 }); + } catch (error: any) { + return NextResponse.json({ error: error?.message }, { status: 500 }); + } +} diff --git a/app/api/forums/route.ts b/app/api/forums/route.ts index 2e7fa85..53c5627 100644 --- a/app/api/forums/route.ts +++ b/app/api/forums/route.ts @@ -1,5 +1,6 @@ import { getServerSession } from 'next-auth'; import { NextRequest, NextResponse } from 'next/server'; +import mongoose from 'mongoose'; import startDb from '@/lib/db'; import Forum from '@/models/Forum'; import { authOptions } from '../auth/[...nextauth]/authOptions'; @@ -19,16 +20,25 @@ export async function GET(req: NextRequest) { const tags = req.nextUrl.searchParams.get('tag')?.toString() ?? ''; if (_id) { - const forum = await Forum.find({ _id }).populate('user'); - return NextResponse.json(forum, { status: 200 }); + const forum = await Forum.findById(_id).populate({ path: 'user', select: 'name' }); + const answers = await Forum.find({ parent: _id }).populate({ path: 'user', select: 'name' }); + if (!forum?.views.includes(new mongoose.Types.ObjectId(session?.user._id))) { + await forum?.updateOne({ $push: { views: session?.user._id } }, { new: true }); + } + return NextResponse.json([forum, answers], { status: 200 }); } if (tags) { - const forums = await Forum.find({ tags }).populate('user').sort('-updatedAt'); + const forums = await Forum.find({ tags }) + .populate({ path: 'user', select: 'name' }) + .sort('-updatedAt'); return NextResponse.json(forums, { status: 200 }); } - const forums = await Forum.find({}).populate('user').sort('-updatedAt'); + const forums = await Forum.find({ question: { $exists: true }, description: { $exists: true } }) + .populate({ path: 'user', select: 'name' }) + .sort('-updatedAt'); + return NextResponse.json(forums, { status: 200 }); } catch (error: any) { return NextResponse.json({ error: error?.message }, { status: 500 }); @@ -58,7 +68,7 @@ export async function PUT(req: NextRequest) { } const body = await req.json(); await startDb(); - const forum = await Forum.findByIdAndUpdate(body._id, body); + const forum = await Forum.findByIdAndUpdate(body._id, body, { new: true }); return NextResponse.json(forum, { status: 200 }); } catch (error: any) { return NextResponse.json({ error: error?.message }, { status: 500 }); @@ -73,6 +83,7 @@ export async function DELETE(req: NextRequest) { } await startDb(); await Forum.findByIdAndDelete(req.nextUrl.searchParams.get('_id')); + await Forum.deleteMany({ parent: req.nextUrl.searchParams.get('_id') }); return NextResponse.json(null, { status: 200 }); } catch (error: any) { return NextResponse.json({ error: error?.message }, { status: 500 }); diff --git a/components/Forum/Forum.tsx b/components/Forum/Forum.tsx index a4d604d..f44398e 100644 --- a/components/Forum/Forum.tsx +++ b/components/Forum/Forum.tsx @@ -1,9 +1,27 @@ -import { Avatar, Badge, Button, Divider, Group, Paper, rem, Stack, Text } from '@mantine/core'; +import { + ActionIcon, + Avatar, + Badge, + Button, + Divider, + Group, + Paper, + rem, + Stack, + Text, +} from '@mantine/core'; import { useSession } from 'next-auth/react'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { useRouter } from 'next/navigation'; -import { IconMessageReply, IconQuestionMark } from '@tabler/icons-react'; +import { + IconBookmark, + IconBookmarkFilled, + IconChevronDown, + IconChevronUp, + IconMessageReply, + IconQuestionMark, +} from '@tabler/icons-react'; import { ForumType } from './Forum.types'; import { apiCall, openModal } from '@/lib/client_functions'; @@ -11,12 +29,23 @@ dayjs.extend(relativeTime); interface Props { forum: ForumType; + refetch: () => void; } -export const Forum = ({ forum }: Props) => { +export const Forum = ({ forum, refetch }: Props) => { const session: any = useSession(); const router = useRouter(); + // check if user is upvoted or downvoted + const isUpVoted = forum?.upvotes?.includes(session?.data?.user?._id); + const isDownVoted = forum?.downvotes?.includes(session?.data?.user?._id); + const isSaved = forum?.saved?.includes(session?.data?.user?._id); + + // remove user from upvotes and downvotes + const removeFromUpVotes = forum?.upvotes?.filter((_id) => _id !== session?.data?.user?._id); + const removeFromDownVotes = forum?.downvotes?.filter((_id) => _id !== session?.data?.user?._id); + const removeFromSaved = forum?.saved?.filter((_id) => _id !== session?.data?.user?._id); + const onDelete = () => openModal('This forum will be deleted permanently', () => { apiCall(`/api/forums?_id=${forum?._id}`, {}, 'DELETE').then(() => { @@ -24,97 +53,173 @@ export const Forum = ({ forum }: Props) => { }); }); + const onUpVote = async () => { + const body = { ...forum }; + if (isDownVoted) { + body.downvotes = removeFromDownVotes; + } else { + body.upvotes = [...forum.upvotes, session?.data?.user?._id]; + } + await apiCall('/api/forums', body, 'PUT'); + refetch(); + }; + + const onDownVote = async () => { + const body = { ...forum }; + if (isUpVoted) { + body.upvotes = removeFromUpVotes; + } else { + body.downvotes = [...forum.downvotes, session?.data?.user?._id]; + } + await apiCall('/api/forums', body, 'PUT'); + refetch(); + }; + + const onBookmark = async () => { + const body = { ...forum }; + if (isSaved) { + body.saved = removeFromSaved; + } else { + body.saved = [...forum.saved, session?.data?.user?._id]; + } + await apiCall('/api/forums', body, 'PUT'); + refetch(); + }; + + if (!forum) return <>; + return ( - - - - - - Asked {dayjs(forum?.createdAt).fromNow()} - - - Modified {dayjs(forum?.updatedAt).fromNow()} - - - Viewed {forum?.views?.length} times - - - - - - {forum?.tags?.map((tag) => ( - router.push(`/forum/tagged/${tag}`)} - style={{ cursor: 'pointer' }} - > - {tag} - - ))} - - - - - {session.data.user._id === forum?.user?._id && ( - <> - - - - )} - {session.data.user._id !== forum?.user?._id && ( + + + + + + + {(forum?.upvotes?.length || 0) - (forum?.downvotes?.length || 0)} + + + + + {isSaved ? : } + + + + {forum?.question && ( + <> + + + + Asked {dayjs(forum?.createdAt).fromNow()} + + + Modified {dayjs(forum?.updatedAt).fromNow()} + + + Viewed {forum?.views?.length} times + + + + + )} + + + {forum?.tags?.map((tag) => ( + router.push(`/forum/tagged/${tag}`)} + style={{ cursor: 'pointer' }} + > + {tag} + + ))} + + + - )} + {session.data.user._id === forum?.user?._id && ( + <> + + + + )} + {session.data.user._id !== forum?.user?._id && ( + + )} + + + + + {forum?.question ? 'asked' : 'answered'} on{' '} + {dayjs(forum?.createdAt).format('MMM DD YYYY @ HH:mm')} + + + + + {forum?.user?.name} + + + } + > + 0 + + + } + > + 0 + + + + + + - - - - asked {dayjs(forum?.createdAt).format('MMM DD YYYY @ HH:mm')} - - - - - {forum?.user?.name} - - - } - > - 0 - - - } - > - 0 - - - - - - - - + + ); }; diff --git a/components/Forum/ReplyToQuestion.tsx b/components/Forum/ReplyToQuestion.tsx new file mode 100644 index 0000000..5cbe00a --- /dev/null +++ b/components/Forum/ReplyToQuestion.tsx @@ -0,0 +1,50 @@ +import { Button, Paper, Text } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { nprogress } from '@mantine/nprogress'; +import Document from '../Document'; +import { apiCall, failure } from '@/lib/client_functions'; + +interface Props { + parent: string; + refetch: () => void; +} + +export const ReplyToQuestion = ({ parent, refetch }: Props) => { + const form = useForm({ + initialValues: { + description: '', + }, + validate: { + description: (value) => (value.length < 1 ? 'This field is required' : null), + }, + }); + + const onSubmit = async () => { + try { + nprogress.start(); + await apiCall('/api/forums/reply', { description: form.values.description, parent }, 'POST'); + form.reset(); + refetch(); + } catch (error: any) { + failure(error); + } finally { + nprogress.complete(); + } + }; + + return ( + + + Your Answer + + form.setFieldValue('description', str)} + isDocumentPage={false} + /> + + + ); +}; diff --git a/components/Forum/index.ts b/components/Forum/index.ts index c2286e4..48bf34c 100644 --- a/components/Forum/index.ts +++ b/components/Forum/index.ts @@ -1,3 +1,4 @@ export { AskQuestion } from './AskQuestion'; export { Forum } from './Forum'; export { ForumItem } from './ForumItem'; +export { ReplyToQuestion } from './ReplyToQuestion'; diff --git a/models/Forum.ts b/models/Forum.ts index 54b0987..25c8ac4 100644 --- a/models/Forum.ts +++ b/models/Forum.ts @@ -7,11 +7,11 @@ export interface ForumDocument extends Document { parent?: Types.ObjectId; user?: Types.ObjectId | UserDocument; tags: string[]; - upvotes?: Types.ObjectId[]; - downvotes?: Types.ObjectId[]; - views?: Types.ObjectId[]; - saved?: Types.ObjectId[]; - answers?: number; + upvotes: Types.ObjectId[]; + downvotes: Types.ObjectId[]; + views: Types.ObjectId[]; + saved: Types.ObjectId[]; + answers: number; createdAt?: Date; updatedAt?: Date; }