From 2780452fec77fa3bdd8623021e344feaef34afdd Mon Sep 17 00:00:00 2001 From: Shivansh Date: Sat, 9 Nov 2024 14:12:33 +0530 Subject: [PATCH 1/2] added backend schema, server actions and testing API for faq forum --- alimento-nextjs/actions/forum/Answer.tsx | 79 ++++++++++ alimento-nextjs/actions/forum/Question.tsx | 141 ++++++++++++++++++ .../faq/[customerId]/[questionId]/route.ts | 39 +++++ .../app/api/testing/faq/[customerId]/route.ts | 37 +++++ .../[adminId]/answer/[answerId]/route.ts | 36 +++++ .../[adminId]/question/[questionId]/route.ts | 36 +++++ alimento-nextjs/app/api/testing/faq/route.ts | 31 ++++ alimento-nextjs/prisma/schema.prisma | 19 +++ 8 files changed, 418 insertions(+) create mode 100644 alimento-nextjs/actions/forum/Answer.tsx create mode 100644 alimento-nextjs/actions/forum/Question.tsx create mode 100644 alimento-nextjs/app/api/testing/faq/[customerId]/[questionId]/route.ts create mode 100644 alimento-nextjs/app/api/testing/faq/[customerId]/route.ts create mode 100644 alimento-nextjs/app/api/testing/faq/admin/[adminId]/answer/[answerId]/route.ts create mode 100644 alimento-nextjs/app/api/testing/faq/admin/[adminId]/question/[questionId]/route.ts create mode 100644 alimento-nextjs/app/api/testing/faq/route.ts diff --git a/alimento-nextjs/actions/forum/Answer.tsx b/alimento-nextjs/actions/forum/Answer.tsx new file mode 100644 index 0000000..608402d --- /dev/null +++ b/alimento-nextjs/actions/forum/Answer.tsx @@ -0,0 +1,79 @@ +"use server" + +import prismadb from "@/lib/prismadb"; +import { Answer } from "@prisma/client"; +import { UUID } from "crypto"; + +type CreateAnswerResponse = { + success: boolean; + data?: Answer + error?: string; +}; + +export async function createAnswer( + questionId: string, + content: string +): Promise { + try { + + const newAnswer = await prismadb.answer.create({ + data: { + content, + questionId, + }, + }); + + await prismadb.question.update({ + where: { id: questionId }, + data: { answered: true }, + }); + + return { + success: true, + data: newAnswer + }; + } catch (error) { + console.error("Error creating answer:", error); + return { + success: false, + error: "Failed to create answer", + }; + } +} + + + + +type DeleteAnswerResponse = { + success: boolean; + data?: Answer; + error?: string; +}; + +export async function deleteSpecificAnswer(answerId: string): Promise { + try { + const deletedAnswer = await prismadb.answer.delete({ + where: { + id: answerId, + }, + }); + + if (!deletedAnswer) { + return { + success: false, + error: "Error in deleting the answer", + }; + } + + return { + success: true, + data: deletedAnswer, + }; + } catch (error) { + console.error("Error deleting specific answer:", error); + return { + success: false, + error: "Failed to delete specific answer", + }; + } +} \ No newline at end of file diff --git a/alimento-nextjs/actions/forum/Question.tsx b/alimento-nextjs/actions/forum/Question.tsx new file mode 100644 index 0000000..e3aeb90 --- /dev/null +++ b/alimento-nextjs/actions/forum/Question.tsx @@ -0,0 +1,141 @@ +'use server'; + +import prismadb from '@/lib/prismadb'; +import { Answer, Question } from '@prisma/client'; +import { UUID } from 'crypto'; + +interface QuestionWithAnswer extends Question{ + answers?:Answer[] +} + +type QuestionResponse = { + success: boolean; + data?: Question; + error?: string; +}; + +type getQuestionsResponse = { + success: boolean; + data?: QuestionWithAnswer[]; + error?: string; +}; + + +export async function createQuestion( + content: string +): Promise { + try { + const newQuestion = await prismadb.question.create({ + data: { + content, + }, + }); + + return { + success: true, + data: newQuestion, + }; + } catch (error) { + console.error('Error creating question:', error); + return { + success: false, + error: 'Failed to create question', + }; + } +} + +export async function getUnansweredQuestions( +): Promise { + try { + const unAnsweredQuestions = await prismadb.question.findMany({ + where: { + answered: false, + }, + }); + + if (!unAnsweredQuestions.length) { + return { + success: false, + error: 'No unanswered questions!', + }; + } + + return { + success: true, + data: unAnsweredQuestions, + }; + } catch (error) { + console.error('Error fetching unanswered questions', error); + return { + success: false, + error: 'Failed to fetch unanswered questions', + }; + } +} + + +export async function getAnsweredQuestions( +): Promise { + try { + const AnsweredQuestions = await prismadb.question.findMany({ + where: { + answered: true, + }, + include:{ + answers:true + } + }); + + if (!AnsweredQuestions.length) { + return { + success: false, + error: 'No unanswered questions!', + }; + } + + return { + success: true, + data: AnsweredQuestions, + }; + } catch (error) { + console.error('Error fetching answered questions', error); + return { + success: false, + error: 'Failed to fetch answered questions', + }; + } +} + +// This will be only accessible by the admin + +// deletes a question and all its related answers if present + +export async function deleteQuestion( + questionId: string, +): Promise { + try { + const deletedQuestion = await prismadb.question.delete({ + where: { + id:questionId + }, + }); + + if (!deletedQuestion) { + return { + success: false, + error: 'error in deleting the question', + }; + } + + return { + success: true, + data: deletedQuestion, + }; + } catch (error) { + console.error('Error deleting specific question', error); + return { + success: false, + error: 'Failed to delete specific question', + }; + } +} \ No newline at end of file diff --git a/alimento-nextjs/app/api/testing/faq/[customerId]/[questionId]/route.ts b/alimento-nextjs/app/api/testing/faq/[customerId]/[questionId]/route.ts new file mode 100644 index 0000000..f5b99ac --- /dev/null +++ b/alimento-nextjs/app/api/testing/faq/[customerId]/[questionId]/route.ts @@ -0,0 +1,39 @@ + +import { createAnswer } from "@/actions/forum/Answer"; +import { createQuestion, getUnansweredQuestions } from "@/actions/forum/Question"; +import { NextRequest, NextResponse } from "next/server"; + +export type Params = Promise<{ + customerId: string; + questionId : string +}>; + +export async function POST( + request: NextRequest, + { params }: { params: Params } +) { + const { customerId , questionId } = await params; + + if (!customerId) { + return new NextResponse('customer not authenticated', { status: 401 }); + } + + const { content } = await request.json(); + + if (!content || !questionId) { + return new NextResponse('Content and questionId is required', { status: 400 }); + } + + try { + const resp = await createAnswer(questionId,content); + + if (resp.success) { + return NextResponse.json(resp.data); + } else { + return NextResponse.json({ err: resp.error }, { status: 400 }); + } + } catch (err) { + console.log('[CREATE_ANSWER]', err); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/alimento-nextjs/app/api/testing/faq/[customerId]/route.ts b/alimento-nextjs/app/api/testing/faq/[customerId]/route.ts new file mode 100644 index 0000000..a615627 --- /dev/null +++ b/alimento-nextjs/app/api/testing/faq/[customerId]/route.ts @@ -0,0 +1,37 @@ + +import { createQuestion, getUnansweredQuestions } from "@/actions/forum/Question"; +import { NextRequest, NextResponse } from "next/server"; + +export type Params = Promise<{ + customerId: string; +}>; + +export async function POST( + request: NextRequest, + { params }: { params: Params } +) { + const { customerId } = await params; + + if (!customerId) { + return new NextResponse('customer not authenticated', { status: 401 }); + } + + const { content } = await request.json(); + + if (!content) { + return new NextResponse('Content is required', { status: 400 }); + } + + try { + const resp = await createQuestion(content); + + if (resp.success) { + return NextResponse.json(resp.data); + } else { + return NextResponse.json({ err: resp.error }, { status: 400 }); + } + } catch (err) { + console.log('[CREATE_QUESTION]', err); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/alimento-nextjs/app/api/testing/faq/admin/[adminId]/answer/[answerId]/route.ts b/alimento-nextjs/app/api/testing/faq/admin/[adminId]/answer/[answerId]/route.ts new file mode 100644 index 0000000..624e45d --- /dev/null +++ b/alimento-nextjs/app/api/testing/faq/admin/[adminId]/answer/[answerId]/route.ts @@ -0,0 +1,36 @@ + +import { deleteSpecificAnswer } from "@/actions/forum/Answer"; +import { NextRequest, NextResponse } from "next/server"; + +export type Params = Promise<{ + userId: string; + answerId: string; +}>; + +export async function DELETE( + request: NextRequest, + { params }: { params: Params } +) { + const { userId, answerId } = await params; + + if (!userId) { + return new NextResponse('User not authenticated', { status: 401 }); + } + + if (!answerId) { + return new NextResponse('Answer ID is required', { status: 400 }); + } + + try { + const resp = await deleteSpecificAnswer(answerId); + + if (resp.success) { + return NextResponse.json(resp.data); + } else { + return NextResponse.json({ err: resp.error }, { status: 400 }); + } + } catch (err) { + console.log('[DELETE_ANSWER]', err); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/alimento-nextjs/app/api/testing/faq/admin/[adminId]/question/[questionId]/route.ts b/alimento-nextjs/app/api/testing/faq/admin/[adminId]/question/[questionId]/route.ts new file mode 100644 index 0000000..86e88d2 --- /dev/null +++ b/alimento-nextjs/app/api/testing/faq/admin/[adminId]/question/[questionId]/route.ts @@ -0,0 +1,36 @@ + +import { deleteQuestion } from "@/actions/forum/Question"; +import { NextRequest, NextResponse } from "next/server"; + +export type Params = Promise<{ + adminId: string; + questionId: string; +}>; + +export async function DELETE( + request: NextRequest, + { params }: { params: Params } +) { + const { adminId, questionId } = await params; + + if (!adminId) { + return new NextResponse('Admin not authenticated', { status: 401 }); + } + + if (!questionId) { + return new NextResponse('Question ID is required', { status: 400 }); + } + + try { + const resp = await deleteQuestion(questionId); + + if (resp.success) { + return NextResponse.json(resp.data); + } else { + return NextResponse.json({ err: resp.error }, { status: 400 }); + } + } catch (err) { + console.log('[DELETE_QUESTION]', err); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/alimento-nextjs/app/api/testing/faq/route.ts b/alimento-nextjs/app/api/testing/faq/route.ts new file mode 100644 index 0000000..17edaea --- /dev/null +++ b/alimento-nextjs/app/api/testing/faq/route.ts @@ -0,0 +1,31 @@ + +import { getAnsweredQuestions, getUnansweredQuestions } from "@/actions/forum/Question"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + try { + // Extract the query parameter + const url = new URL(request.url); + const questionStatus = url.searchParams.get("question"); + + // Check if the query parameter is 'answered' or 'unanswered' + let resp; + + if (questionStatus === "answered") { + resp = await getAnsweredQuestions(); + } else if (questionStatus === "unanswered") { + resp = await getUnansweredQuestions(); + } else { + return new NextResponse("Invalid query parameter. Use '?question=answered' or '?question=unanswered'.", { status: 400 }); + } + + if (resp.success) { + return NextResponse.json(resp.data); + } else { + return NextResponse.json({ err: resp.error }, { status: 400 }); + } + } catch (err) { + console.log('[GET_QUESTIONS]', err); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/alimento-nextjs/prisma/schema.prisma b/alimento-nextjs/prisma/schema.prisma index c5ef52b..08f0f1c 100644 --- a/alimento-nextjs/prisma/schema.prisma +++ b/alimento-nextjs/prisma/schema.prisma @@ -79,3 +79,22 @@ model Image { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + + +model Question { + id String @id @default(cuid()) @map("_id") + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + answers Answer[] @relation("QuestionAnswers") // Removed onDelete: Cascade from here + answered Boolean @default(false) +} + +model Answer { + id String @id @default(cuid()) @map("_id") + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + questionId String + question Question @relation("QuestionAnswers", fields: [questionId], references: [id], onDelete: Cascade) // Moved onDelete: Cascade to this side +} From ccdbf6c9ddee6df8e39d971faae3687deb0b4368 Mon Sep 17 00:00:00 2001 From: Shivansh Date: Sat, 9 Nov 2024 14:41:43 +0530 Subject: [PATCH 2/2] added fully responsive faq frontend page --- .../{ => (footerPages)}/contact-us/page.tsx | 0 .../{ => (footerPages)}/contributors/page.tsx | 0 .../forum/components/NewQuestionForm.tsx | 80 ++++++++++++++++ .../forum/components/unAnsweredQuestions.tsx | 93 +++++++++++++++++++ .../(footerPages)/forum/page.tsx | 77 +++++++++++++++ .../{ => (footerPages)}/policy/page.tsx | 0 .../terms-conditions/page.tsx | 0 alimento-nextjs/components/common/footer.tsx | 1 + alimento-nextjs/components/ui/spinner.tsx | 45 +++++++++ alimento-nextjs/components/ui/textarea.tsx | 22 +++++ 10 files changed, 318 insertions(+) rename alimento-nextjs/app/(PublicRoutes)/{ => (footerPages)}/contact-us/page.tsx (100%) rename alimento-nextjs/app/(PublicRoutes)/{ => (footerPages)}/contributors/page.tsx (100%) create mode 100644 alimento-nextjs/app/(PublicRoutes)/(footerPages)/forum/components/NewQuestionForm.tsx create mode 100644 alimento-nextjs/app/(PublicRoutes)/(footerPages)/forum/components/unAnsweredQuestions.tsx create mode 100644 alimento-nextjs/app/(PublicRoutes)/(footerPages)/forum/page.tsx rename alimento-nextjs/app/(PublicRoutes)/{ => (footerPages)}/policy/page.tsx (100%) rename alimento-nextjs/app/(PublicRoutes)/{ => (footerPages)}/terms-conditions/page.tsx (100%) create mode 100644 alimento-nextjs/components/ui/spinner.tsx create mode 100644 alimento-nextjs/components/ui/textarea.tsx diff --git a/alimento-nextjs/app/(PublicRoutes)/contact-us/page.tsx b/alimento-nextjs/app/(PublicRoutes)/(footerPages)/contact-us/page.tsx similarity index 100% rename from alimento-nextjs/app/(PublicRoutes)/contact-us/page.tsx rename to alimento-nextjs/app/(PublicRoutes)/(footerPages)/contact-us/page.tsx diff --git a/alimento-nextjs/app/(PublicRoutes)/contributors/page.tsx b/alimento-nextjs/app/(PublicRoutes)/(footerPages)/contributors/page.tsx similarity index 100% rename from alimento-nextjs/app/(PublicRoutes)/contributors/page.tsx rename to alimento-nextjs/app/(PublicRoutes)/(footerPages)/contributors/page.tsx diff --git a/alimento-nextjs/app/(PublicRoutes)/(footerPages)/forum/components/NewQuestionForm.tsx b/alimento-nextjs/app/(PublicRoutes)/(footerPages)/forum/components/NewQuestionForm.tsx new file mode 100644 index 0000000..27f4778 --- /dev/null +++ b/alimento-nextjs/app/(PublicRoutes)/(footerPages)/forum/components/NewQuestionForm.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { createQuestion } from '@/actions/forum/Question'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Spinner } from '@/components/ui/spinner'; +import { Textarea } from '@/components/ui/textarea'; +import { useSession } from 'next-auth/react'; +import { SyntheticEvent, useEffect, useState } from 'react'; + +const NewQuestionForm = () => { + const [questionContent, setQuestionContent] = useState(''); + const [submissionError, setSubmissionError] = useState(''); + + const [isMounted, setIsMounted] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const session = useSession(); + + useEffect(() => { + setIsMounted(true); + if (!(session.status === 'loading')) { + if (session.status === 'authenticated') { + setIsAuthenticated(true); + // console.log("hereeeeeeeeee"+isAuthenticated+session.status) + } + } + }, [session.status]); + + if (!isMounted) { + return ; + } + + const handleSubmit = async (e: SyntheticEvent) => { + // e.preventDefault(); + try { + const response = await createQuestion(questionContent); + if (!response.success && response.error) { + setSubmissionError(response.error); + } else { + setQuestionContent(''); + setSubmissionError(''); + } + } catch (error) { + setSubmissionError( + 'An error occurred while submitting your question. Please try again.' + ); + } + }; + + return ( + <> + {isAuthenticated ? ( +
+ +