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
@@ -7,6 +7,7 @@ import { KinescopePlayer } from "@/components/player/kinescope-player";
import { LessonContent } from "@/components/student/lesson-content";
import { LessonCompleteButton } from "@/components/student/lesson-complete-button";
import { HomeworkSection } from "@/components/student/homework-section";
import { QuizSection } from "@/components/student/quiz-section";
import { LessonComments } from "@/components/student/lesson-comments";
import { FileFormatBadge } from "@/components/shared/file-format-badge";
@@ -26,6 +27,9 @@ export default async function LessonPage({ params }: Props) {
include: {
files: { orderBy: { createdAt: "asc" } },
homework: true,
quiz: {
include: { questions: { orderBy: { order: "asc" } } },
},
module: {
include: {
course: {
@@ -71,6 +75,12 @@ export default async function LessonPage({ params }: Props) {
})
: null;
const quizAttempt = lesson?.quiz && session && !isAdmin
? await prisma.quizAttempt.findFirst({
where: { quizId: lesson.quiz.id, userId: session.user.id },
})
: null;
if (!lesson || lesson.module.course.slug !== slug) notFound();
const isCompleted = !!progress;
@@ -163,6 +173,21 @@ export default async function LessonPage({ params }: Props) {
</div>
)}
{/* Quiz */}
{lesson.quiz && !isAdmin && (
<div className="mb-8">
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--muted-foreground)" }}>
Тест
</p>
<QuizSection
quiz={lesson.quiz}
attempt={quizAttempt ? { answers: quizAttempt.answers as Record<string, string> } : null}
slug={slug}
lessonId={lessonId}
/>
</div>
)}
{/* Complete button + Prev/Next navigation */}
<div
className="flex items-center justify-between pt-6 mt-6"
@@ -179,10 +204,10 @@ export default async function LessonPage({ params }: Props) {
<div />
)}
{!isAdmin && !lesson.homework && (
{!isAdmin && !lesson.homework && !lesson.quiz && (
<LessonCompleteButton lessonId={lessonId} slug={slug} isCompleted={isCompleted} />
)}
{!isAdmin && lesson.homework && isCompleted && (
{!isAdmin && (lesson.homework || lesson.quiz) && isCompleted && (
<LessonCompleteButton lessonId={lessonId} slug={slug} isCompleted={true} readOnly={true} />
)}