diff --git a/apps/frontend/src/screens/QuizBuilder.tsx b/apps/frontend/src/screens/QuizBuilder.tsx index 5b64394..064859f 100644 --- a/apps/frontend/src/screens/QuizBuilder.tsx +++ b/apps/frontend/src/screens/QuizBuilder.tsx @@ -9,6 +9,7 @@ import { LuUserSearch } from "react-icons/lu" import AiQuizGenerator from "../components/AiQuizGenerator" import type { Difficulty, GeneratedQuestion } from "../services/genaiApi" import { DIFFICULTY_STYLES } from "../components/ai/difficulty" +import { BsPencil } from "react-icons/bs"; const DIFFICULTIES: Difficulty[] = ['Low', 'Medium', 'High']; @@ -55,6 +56,10 @@ function QuizBuilder() { const [loadError, setLoadError] = useState(null); const [codeError, setCodeError] = useState(null); + const [editingQuestion, setEditingQuestion] = + useState(null); + const [editingQuestionId, setEditingQuestionId] = + useState(null); @@ -155,36 +160,80 @@ function QuizBuilder() { }; - const handleSaveQuestion = async () => { - if (!qText.trim() || answers.some(a => !a.text.trim())) return alert('Fill all text fields'); - if (!answers.some(a => a.isCorrect)) return alert('Select at least one correct answer'); +const handleSaveQuestion = async () => { + if (!qText.trim() || answers.some((a) => !a.text.trim())) { + return alert("Fill all text fields"); + } - const seconds = qTime.trim() === '' ? 15 : Number(qTime); - if (!Number.isInteger(seconds) || seconds < MIN_TIME || seconds > MAX_TIME) { - return setSaveError(`Seconds must be a whole number from ${MIN_TIME} to ${MAX_TIME}`); - } + if (!answers.some((a) => a.isCorrect)) { + return alert("Select at least one correct answer"); + } - try { - setSaveError(''); - await api.post(`/quizzes/${id}/questions`, { - text: qText, - timeLimit: seconds, - difficulty: qDifficulty, - answers - }); - setShowAddQ(false); - setQText(''); - setQTime('15'); - setQDifficulty('Medium'); - setAnswers([{ text: '', isCorrect: true }, { text: '', isCorrect: false }, { text: '', isCorrect: false }, { text: '', isCorrect: false }]); - fetchQuiz(); - } catch (err: unknown) { - console.error('Failed to save question', err); - const resp = (err as { response?: { data?: { error?: string } } }).response; - setSaveError(resp?.data?.error || 'Failed to save question'); + const seconds = qTime.trim() === "" ? 15 : Number(qTime); + + if ( + !Number.isInteger(seconds) || + seconds < MIN_TIME || + seconds > MAX_TIME + ) { + return setSaveError( + `Seconds must be a whole number from ${MIN_TIME} to ${MAX_TIME}` + ); + } + + try { + setSaveError(""); + + const payload = { + text: qText, + timeLimit: seconds, + difficulty: qDifficulty, + answers, + }; + + if (editingQuestion) { + // UPDATE EXISTING QUESTION + await api.put( + `/quizzes/${id}/questions/${editingQuestion.id}`, + payload + ); + } else { + // CREATE NEW QUESTION + await api.post( + `/quizzes/${id}/questions`, + payload + ); } - }; + // Reset form + setEditingQuestion(null); + setEditingQuestionId(null); + setShowAddQ(false); + setQText(""); + setQTime("15"); + setQDifficulty("Medium"); + setAnswers([ + { text: "", isCorrect: true }, + { text: "", isCorrect: false }, + { text: "", isCorrect: false }, + { text: "", isCorrect: false }, + ]); + + fetchQuiz(); + } catch (err: unknown) { + console.error(err); + + const resp = (err as { + response?: { + data?: { + error?: string; + }; + }; + }).response; + + setSaveError(resp?.data?.error || "Failed to save question"); + } +}; const handleDeleteQuestion = async () => { if (!questionToDelete || deleting) return; try { @@ -211,6 +260,15 @@ function QuizBuilder() { } }; + const handleEditQuestion = (q: QuizQuestion) => { + setEditingQuestion(q); + + setQText(q.text); + setQTime(String(q.timeLimit)); + setQDifficulty(q.difficulty ?? "Medium"); + setAnswers(q.answers); + setShowAddQ(true); +}; return (
@@ -283,7 +341,22 @@ function QuizBuilder() { -
@@ -360,7 +433,7 @@ function QuizBuilder() {
- +
)} @@ -374,7 +447,142 @@ function QuizBuilder() { )} {quiz?.questions?.map((q: any, idx: number) => (
-
+ + { +editingQuestionId === q.id ? (
+ +

+ Editing Question +

+ + {/* Question */} +
+ setQText(e.target.value)} + /> + +
+ setQTime(e.target.value)} + /> + + + Seconds + +
+
+ + {/* Difficulty */} +
+ + DIFFICULTY + + + {DIFFICULTIES.map((d) => ( + + ))} +
+ + {/* Answers */} +
+ {answers.map((ans, idx) => ( +
+ + + + setAnswers( + answers.map((a, i) => + i === idx + ? { ...a, text: e.target.value } + : a + ) + ) + } + /> +
+ ))} +
+ + {saveError && ( +

+ {saveError} +

+ )} + + {/* Buttons */} +
+ + + +
+ +
):(

{idx + 1}. {q.text}

@@ -383,6 +591,16 @@ function QuizBuilder() { )} {q.timeLimit}s +
-
+
)} +
{q.answers.map((a: any) => (
diff --git a/apps/http-server/src/controllers/quizController.ts b/apps/http-server/src/controllers/quizController.ts index cbcff8f..1078c1e 100644 --- a/apps/http-server/src/controllers/quizController.ts +++ b/apps/http-server/src/controllers/quizController.ts @@ -436,3 +436,108 @@ export const deleteQuestionController = async (req: Request, res: Response) => { return errorResponse(res, 500, "Internal Server Error"); } } + + + + +export const updateQuestionController = async ( + req: Request, + res: Response +) => { + try { + const userId = req.userId; + const quizId = req.params.quizId as string; + const questionId = req.params.questionId as string; + + if (!userId) { + return errorResponse(res, 401, "Unauthorized"); + } + + if (!questionId || !questionId.trim()) { + return errorResponse(res, 400, "Question ID is required"); + } + + const parsedData = parseCreateQuestion(req.body); + if (!parsedData.ok) { + return errorResponse(res, 400, parsedData.error); + } + + const { text, timeLimit, answers, difficulty } = parsedData.data; + + const question = await prisma.question.findUnique({ + where: { + id: questionId + }, + include: { + quiz: { + include: { + quizOrganizers: { + where: { + userId, + inviteStatus: "ACCEPTED" + } + } + } + } + } + }); + + if (!question) { + return errorResponse(res, 404, "Question not found"); + } + + if (question.quizId !== quizId) { + return errorResponse(res, 404, "Question not found in this quiz"); + } + + const isOrganizer = question.quiz.quizOrganizers.length > 0; + const isCreator = question.quiz.creatorId === userId; + + if (!isOrganizer && !isCreator) { + return errorResponse(res, 403, "You don't have access to this quiz"); + } + + const updatedQuestion = await prisma.$transaction(async (tx) => { + await tx.$queryRaw`SELECT "id" FROM "Quiz" WHERE "id" = ${question.quizId} FOR UPDATE`; + +await tx.question.update({ + where: { + id: question.id + }, + data: { + text, + timeLimit, + difficulty + } +}); + +await tx.answer.deleteMany({ + where: { + questionId: question.id + } +}); + +await tx.answer.createMany({ + data: answers.map((answer) => ({ + questionId: question.id, + text: answer.text, + isCorrect: answer.isCorrect + })) +}); + +return await tx.question.findUnique({ + where: { + id: question.id + }, + include: { + answers: true + } +}); + }); + + return res.status(200).json(updatedQuestion); + } catch (error) { + console.log("Update question error: ", error); + return errorResponse(res, 500, "Internal Server Error"); + } +}; \ No newline at end of file diff --git a/apps/http-server/src/routes/quiz.ts b/apps/http-server/src/routes/quiz.ts index 3517e70..08bbc9c 100644 --- a/apps/http-server/src/routes/quiz.ts +++ b/apps/http-server/src/routes/quiz.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import { authMiddleware } from "../middlewares/authmiddleware"; -import { createQuestionController, createQuizController, deleteQuestionController, generateAccessCodeController, getAllQuizesController, getQuizByIdController, startQuizSessionController } from "../controllers/quizController"; +import { createQuestionController, createQuizController, deleteQuestionController, generateAccessCodeController, getAllQuizesController, getQuizByIdController, startQuizSessionController ,updateQuestionController,} from "../controllers/quizController"; const router: Router = Router(); @@ -36,4 +36,12 @@ router.delete("/questions/:questionId", authMiddleware, deleteQuestionController router.post("/:quizId/start-session", authMiddleware, startQuizSessionController) +//to edit a created question +router.put( + "/:quizId/questions/:questionId", + authMiddleware, + updateQuestionController +); + + export default router; \ No newline at end of file diff --git a/setup.md b/setup.md index 06e1ea7..0b09649 100644 --- a/setup.md +++ b/setup.md @@ -83,7 +83,7 @@ both services use the same signing secret. Compose also starts Redis and sets its connection variables. You do not need a separate `apps/ws-server/.env` for `docker compose up`. -*Create a `.env` file in `/apps/genAI` (copy from `.env.example`) +*Create a `.env` file in `/apps/genAI` (copy from `.env.example`) ```bash cp apps/genAI/.env.example apps/genAI/.env ```