Add quiz feature: student UI, admin editor, lesson page integration

- QuizSection component: shows questions as text inputs, read-only after submission
- QuizEditor component: admin CRUD for quiz questions with type selector
- saveQuiz/deleteQuiz server actions for admin
- submitQuizAttempt server action: idempotent, auto-marks lesson complete
- Student lesson page: renders QuizSection, updates complete button logic
- Admin lesson page: renders QuizEditor below homework section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 11:43:16 +05:00
parent 3ed7bc147b
commit d2150153df
6 changed files with 412 additions and 3 deletions
+53
View File
@@ -0,0 +1,53 @@
"use server";
import { prisma } from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
async function requireAdmin() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session || session.user.role !== "admin") throw new Error("Forbidden");
}
export async function saveQuiz(
lessonId: string,
questions: { text: string; type: "TEXT" | "SINGLE" | "MULTIPLE"; order: number }[]
) {
await requireAdmin();
const existing = await prisma.quiz.findUnique({ where: { lessonId } });
if (existing) {
await prisma.quizQuestion.deleteMany({ where: { quizId: existing.id } });
await prisma.quizQuestion.createMany({
data: questions.map((q) => ({
quizId: existing.id,
text: q.text,
type: q.type,
order: q.order,
})),
});
} else {
await prisma.quiz.create({
data: {
lessonId,
questions: {
create: questions.map((q) => ({
text: q.text,
type: q.type,
order: q.order,
})),
},
},
});
}
revalidatePath(`/admin`);
}
export async function deleteQuiz(lessonId: string) {
await requireAdmin();
await prisma.quiz.delete({ where: { lessonId } });
revalidatePath(`/admin`);
}
+37
View File
@@ -94,6 +94,43 @@ export async function submitHomework(
revalidatePath(`/courses/${slug}/lessons/${lessonId}`);
}
// ── Quiz ──────────────────────────────────────────────────────────────────────
export async function submitQuizAttempt(
quizId: string,
lessonId: string,
slug: string,
answers: Record<string, string>
) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) throw new Error("Unauthorized");
const existing = await prisma.quizAttempt.findFirst({
where: { quizId, userId: session.user.id },
});
if (existing) return;
await prisma.$transaction(async (tx) => {
await tx.quizAttempt.create({
data: {
userId: session.user.id,
quizId,
score: 0,
answers: answers as object,
},
});
await tx.lessonProgress.upsert({
where: { userId_lessonId: { userId: session.user.id, lessonId } },
create: { userId: session.user.id, lessonId },
update: {},
});
});
revalidatePath(`/courses/${slug}/lessons/${lessonId}`);
revalidatePath(`/courses/${slug}`);
revalidatePath("/dashboard");
}
// ── Comments ──────────────────────────────────────────────────────────────────
export async function addComment(lessonId: string, slug: string, text: string) {