diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 1e96fa7..8ed70ed 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -25,10 +25,23 @@ model User {
passwordReset PasswordResetToken?
examSubmissions ExamSubmission[]
ExamProgress ExamProgress[]
+ QuestionBank QuestionBank?
@@map("users")
}
+model QuestionBank {
+ id String @id @default(cuid())
+ userId String @unique
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ examId String @unique
+ exam Exam @relation(fields: [examId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@map("question_banks")
+}
+
enum Role {
USER
ADMIN
@@ -95,11 +108,14 @@ model Exam {
description String
price Int
duration Int
+ shuffleQues Boolean @default(false)
+ published Boolean @default(true)
questions Question[]
submissions ExamSubmission[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ExamProgress ExamProgress[]
+ QuestionBank QuestionBank?
@@map("exams")
}
diff --git a/src/actions/auth-actions.ts b/src/actions/auth-actions.ts
index d53c715..1316cae 100644
--- a/src/actions/auth-actions.ts
+++ b/src/actions/auth-actions.ts
@@ -49,6 +49,20 @@ export const signUp = async (values: signUpValues) => {
data: { username, email, hashedPassword: passwordHash, displayName },
})
+ const customExam = await db.exam.create({
+ data: {
+ title: 'Custom Test',
+ description: 'This is a custom Test from your own Question Bank',
+ duration: 120,
+ price: 0,
+ published: false,
+ },
+ })
+
+ await db.questionBank.create({
+ data: { userId: user.id, examId: customExam.id },
+ })
+
const code = generateVerificationCode()
await db.verificationEmail.create({
diff --git a/src/actions/exams.ts b/src/actions/exams.ts
index 1abdd3b..4e76eb8 100644
--- a/src/actions/exams.ts
+++ b/src/actions/exams.ts
@@ -7,6 +7,9 @@ import { cache } from 'react'
export const getExams = cache(async () => {
const response = await db.exam.findMany({
+ where: {
+ published: true,
+ },
select: {
id: true,
title: true,
@@ -17,6 +20,25 @@ export const getExams = cache(async () => {
})
return response
})
+export const getExamsQues = cache(async (examId: string) => {
+ const response = await db.exam.findUnique({
+ where: {
+ id: examId,
+ },
+ select: {
+ questions: {
+ select: {
+ id: true,
+ text: true,
+ correctAnswer: true,
+ options: true,
+ },
+ },
+ },
+ })
+
+ return response
+})
interface SubmitExamParams {
examId: string
diff --git a/src/actions/question-bank.ts b/src/actions/question-bank.ts
new file mode 100644
index 0000000..fcc4552
--- /dev/null
+++ b/src/actions/question-bank.ts
@@ -0,0 +1,44 @@
+'use server'
+import { validateRequest } from '@/auth'
+import db from '@/lib/db'
+import type { questionFormType } from '@/schemas'
+
+export const getQuestionBankExam = async () => {
+ const session = await validateRequest()
+ if (!session || !session.user) {
+ throw new Error('Unauthorized')
+ }
+ const questionBankExam = await db.questionBank.findUnique({
+ where: {
+ userId: session.user.id,
+ },
+ include: {
+ exam: true,
+ },
+ })
+ return questionBankExam?.exam || null
+}
+
+export const addQuestionsToBank = async (
+ examId: string,
+ question: questionFormType
+) => {
+ const newQuestion = await db.question.create({
+ data: {
+ examId: examId,
+ text: question.text,
+ options: question.options,
+ correctAnswer: question.correctAnswer,
+ },
+ })
+ return newQuestion
+}
+
+export const deleteQuestion = async (questionid: string) => {
+ const question = await db.question.delete({
+ where: {
+ id: questionid,
+ },
+ })
+ return question
+}
diff --git a/src/app/(main)/(non-exam-section)/available-exams/page.tsx b/src/app/(main)/(non-exam-section)/available-exams/page.tsx
index e7b8413..2058d7e 100644
--- a/src/app/(main)/(non-exam-section)/available-exams/page.tsx
+++ b/src/app/(main)/(non-exam-section)/available-exams/page.tsx
@@ -1,10 +1,14 @@
import { getExams } from '@/actions/exams'
+import { getQuestionBankExam } from '@/actions/question-bank'
import AvailableExams from '@/components/exams/avaiable'
import React from 'react'
const Page = async () => {
const data = await getExams()
- return
+ const customExamdata = await getQuestionBankExam()
+
+ const updatedData = customExamdata ? [customExamdata, ...data] : data
+ return
}
export default Page
diff --git a/src/app/(main)/(non-exam-section)/question-bank/page.tsx b/src/app/(main)/(non-exam-section)/question-bank/page.tsx
new file mode 100644
index 0000000..b0ce34b
--- /dev/null
+++ b/src/app/(main)/(non-exam-section)/question-bank/page.tsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { PreviewQuestion } from '@/components/question-bank/preview_question'
+import { AddQuesitons } from '@/components/question-bank/add-question'
+import { getQuestionBankExam } from '@/actions/question-bank'
+
+export default async function Home() {
+ const customExam = await getQuestionBankExam()
+ return customExam ? (
+
+
Question Bank
+
+
+
+ Question Bank
+
+
+ Add Questions
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ 'Can not fetch Question Bank!!'
+ )
+}
diff --git a/src/components/exams/avaiable.tsx b/src/components/exams/avaiable.tsx
index 9312875..357f2c1 100644
--- a/src/components/exams/avaiable.tsx
+++ b/src/components/exams/avaiable.tsx
@@ -18,9 +18,9 @@ import {
interface Exam {
id: string
title: string
+ duration: number | null
description: string
- duration: number
- price: number
+ price: number | null
}
export default function AvailableExams({ exams }: { exams: Exam[] }) {
@@ -88,7 +88,9 @@ export default function AvailableExams({ exams }: { exams: Exam[] }) {
- INR {exam.price}
+
+ {exam.price === 0 ? 'FREE' : `INR ${exam.price}`}
+
diff --git a/src/components/question-bank/add-question.tsx b/src/components/question-bank/add-question.tsx
new file mode 100644
index 0000000..d90d784
--- /dev/null
+++ b/src/components/question-bank/add-question.tsx
@@ -0,0 +1,126 @@
+'use client'
+import { QuestionFormSchema, questionFormType } from '@/schemas'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm } from 'react-hook-form'
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '../ui/form'
+import { Textarea } from '../ui/textarea'
+import { Button } from '../ui/button'
+import { Input } from '../ui/input'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectValue,
+ SelectTrigger,
+} from '../ui/select'
+import { addQuestionsToBank } from '@/actions/question-bank'
+import { toast } from 'sonner'
+
+export function AddQuesitons({ examId }: { examId: string }) {
+ const form = useForm({
+ resolver: zodResolver(QuestionFormSchema),
+ defaultValues: {
+ text: '',
+ options: ['', '', '', ''],
+ },
+ })
+
+ const onSubmit = async (data: questionFormType) => {
+ try {
+ const res = await addQuestionsToBank(examId, data)
+ toast.success('Question Saved')
+ form.reset()
+ } catch (error) {
+ toast.error('failed! PLease Try Again Later')
+ }
+ }
+ const optionsIndices = Array.from({ length: 4 }, (_, index) => index)
+
+ return (
+
+ )
+}
diff --git a/src/components/question-bank/preview_question.tsx b/src/components/question-bank/preview_question.tsx
new file mode 100644
index 0000000..f81d7b5
--- /dev/null
+++ b/src/components/question-bank/preview_question.tsx
@@ -0,0 +1,115 @@
+'use client'
+import { getExamsQues } from '@/actions/exams'
+import { useEffect, useState } from 'react'
+import { Button } from '../ui/button'
+import { ScrollArea } from '@/components/ui/scroll-area'
+import { RefreshCcw } from 'lucide-react'
+import { Card } from '../ui/card'
+import { toast } from 'sonner'
+import { deleteQuestion } from '@/actions/question-bank'
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog'
+
+export function PreviewQuestion({ examId }: { examId: string }) {
+ interface Questions {
+ questions: {
+ id: string
+ text: string
+ correctAnswer: number
+ options: string[]
+ }[]
+ }
+ const [isloading, setIsLoading] = useState(true)
+ const [Exam, setExam] = useState()
+ useEffect(() => {
+ async function getExams() {
+ const ques = await getExamsQues(examId)
+ setExam(ques)
+ setIsLoading(false)
+ }
+ getExams()
+ }, [examId])
+ const Delete = async (questionId: string) => {
+ try {
+ const ques = await deleteQuestion(questionId)
+ toast.success('Question Deleted SuccessFully')
+ return ques
+ } catch (err) {
+ toast.error('Cannot Delete Now')
+ }
+ return
+ }
+ return isloading ? (
+
+
+
+ Please wait
+
+
+ ) : Exam ? (
+
+
+
+ Please Relode to see the Latest Changes!
+
+ {Exam.questions.map((p) => (
+
+
+
+
+
+ Delete
+
+
+
+
+
+ Are you absolutely sure you want to Delete this question?
+
+
+ This action cannot be undone. This will permanently delete
+ your Question and remove your data from our servers.
+
+
+
+ Cancel
+ Delete(p.id)}>
+ Continue
+
+
+
+
+
+ {p.text}
+
+ {p.options.map((o, index) => (
+
+ ))}
+
+
+ ))}
+
+
+ ) : (
+ Please Add some Questions First
+ )
+}
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..9f9a6dc
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/src/config/index.ts b/src/config/index.ts
index 6fd43d0..a5a8389 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -6,7 +6,8 @@ import {
MedalIcon,
Library,
LucideIcon,
- HomeIcon
+ HomeIcon,
+ MailQuestion,
} from 'lucide-react'
export const applicationName = '100xDevs'
@@ -85,7 +86,6 @@ export const cohort_lists = [
},
]
-
type Submenu = {
href: string
label: string
@@ -136,6 +136,13 @@ export function getMenuList(pathname: string): Group[] {
icon: MedalIcon,
submenus: [],
},
+ {
+ href: '/question-bank',
+ label: 'Question Bank',
+ active: pathname === '/question-bank',
+ icon: MailQuestion,
+ submenus: [],
+ },
],
},
]
diff --git a/src/schemas/index.ts b/src/schemas/index.ts
index 198b352..98b8f14 100644
--- a/src/schemas/index.ts
+++ b/src/schemas/index.ts
@@ -34,7 +34,19 @@ const passwordRequirements = [
const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
-
+export type QuestionBankType = {
+ id: string
+ userId: string
+ examId: string
+ createdAt: Date
+ updatedAt: Date
+}
+export type questionFormType = z.infer
+export const QuestionFormSchema = z.object({
+ text: z.string().min(5),
+ options: z.array(z.string().nonempty(' option must be non-empty string')),
+ correctAnswer: z.number().int().min(0).max(3),
+})
export const signUpSchema = z
.object({
displayName: z