diff --git a/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx b/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx index 9af3191..ef769b5 100644 --- a/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx +++ b/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx @@ -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) { )} + {/* Quiz */} + {lesson.quiz && !isAdmin && ( +
+

+ Тест +

+ } : null} + slug={slug} + lessonId={lessonId} + /> +
+ )} + {/* Complete button + Prev/Next navigation */}
)} - {!isAdmin && !lesson.homework && ( + {!isAdmin && !lesson.homework && !lesson.quiz && ( )} - {!isAdmin && lesson.homework && isCompleted && ( + {!isAdmin && (lesson.homework || lesson.quiz) && isCompleted && ( )} diff --git a/src/app/admin/courses/[courseId]/modules/[moduleId]/lessons/[lessonId]/page.tsx b/src/app/admin/courses/[courseId]/modules/[moduleId]/lessons/[lessonId]/page.tsx index 7522b69..8335c6d 100644 --- a/src/app/admin/courses/[courseId]/modules/[moduleId]/lessons/[lessonId]/page.tsx +++ b/src/app/admin/courses/[courseId]/modules/[moduleId]/lessons/[lessonId]/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { LessonEditor } from "@/components/admin/lesson-editor"; import { LessonFilesManager } from "@/components/admin/lesson-files-manager"; import { HomeworkEditor } from "@/components/admin/homework-editor"; +import { QuizEditor } from "@/components/admin/quiz-editor"; interface Props { params: Promise<{ courseId: string; moduleId: string; lessonId: string }>; @@ -18,6 +19,9 @@ export default async function LessonEditorPage({ params }: Props) { include: { files: { orderBy: { createdAt: "asc" } }, homework: true, + quiz: { + include: { questions: { orderBy: { order: "asc" } } }, + }, module: { include: { course: { select: { title: true, slug: true } } }, }, @@ -40,10 +44,12 @@ export default async function LessonEditorPage({ params }: Props) { const plain = JSON.parse(JSON.stringify({ files: lesson.files, homework: lesson.homework, + quiz: lesson.quiz, siblings, })) as { files: typeof lesson.files; homework: typeof lesson.homework; + quiz: typeof lesson.quiz; siblings: typeof siblings; }; @@ -86,12 +92,20 @@ export default async function LessonEditorPage({ params }: Props) {
{/* Homework section */} -
+

Домашнее задание

+ + {/* Quiz section */} +
+

+ Тест +

+ +
); } diff --git a/src/components/admin/quiz-editor.tsx b/src/components/admin/quiz-editor.tsx new file mode 100644 index 0000000..a344501 --- /dev/null +++ b/src/components/admin/quiz-editor.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { saveQuiz, deleteQuiz } from "@/lib/actions/quiz-actions"; + +type QType = "TEXT" | "SINGLE" | "MULTIPLE"; + +interface Question { + id?: string; + text: string; + type: QType; + order: number; +} + +interface Props { + lessonId: string; + initial: { id: string; questions: Question[] } | null; +} + +const TYPE_LABELS: Record = { + TEXT: "Текстовый ответ", + SINGLE: "Один вариант", + MULTIPLE: "Несколько вариантов", +}; + +export function QuizEditor({ lessonId, initial }: Props) { + const [questions, setQuestions] = useState( + initial?.questions ?? [] + ); + const [saved, setSaved] = useState(false); + const [pending, startTransition] = useTransition(); + + const inputStyle = { + border: "2px solid var(--border)", + background: "var(--background)", + outline: "none", + width: "100%", + padding: "0.5rem 0.75rem", + fontSize: "0.875rem", + fontFamily: "inherit", + }; + + function addQuestion() { + setQuestions((prev) => [ + ...prev, + { text: "", type: "TEXT", order: prev.length }, + ]); + } + + function updateQuestion(idx: number, patch: Partial) { + setQuestions((prev) => + prev.map((q, i) => (i === idx ? { ...q, ...patch } : q)) + ); + } + + function removeQuestion(idx: number) { + setQuestions((prev) => + prev.filter((_, i) => i !== idx).map((q, i) => ({ ...q, order: i })) + ); + } + + function handleSave() { + if (questions.length === 0) return; + startTransition(async () => { + await saveQuiz( + lessonId, + questions.map((q, i) => ({ text: q.text.trim(), type: q.type, order: i })) + ); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }); + } + + function handleDelete() { + if (!confirm("Удалить квиз и все ответы студентов?")) return; + startTransition(async () => { + await deleteQuiz(lessonId); + setQuestions([]); + }); + } + + return ( +
+ {questions.length === 0 && ( +

+ Вопросов нет +

+ )} + + {questions.map((q, idx) => ( +
+
+ + Вопрос {idx + 1} + + +
+ updateQuestion(idx, { text: e.target.value })} + style={{ ...inputStyle, minHeight: "unset" }} + placeholder="Текст вопроса..." + onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} + onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} + /> +
+ {(["TEXT", "SINGLE", "MULTIPLE"] as QType[]).map((t) => ( + + ))} +
+
+ ))} + +
+ + + {initial && ( + + )} + {saved && ( + + ✓ Сохранено + + )} +
+
+ ); +} diff --git a/src/components/student/quiz-section.tsx b/src/components/student/quiz-section.tsx new file mode 100644 index 0000000..698f4b5 --- /dev/null +++ b/src/components/student/quiz-section.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { submitQuizAttempt } from "@/lib/actions/student-actions"; + +interface Question { + id: string; + text: string; + type: "TEXT" | "SINGLE" | "MULTIPLE"; + order: number; +} + +interface Props { + quiz: { id: string; questions: Question[] }; + attempt: { answers: Record } | null; + slug: string; + lessonId: string; +} + +const inputStyle = { + border: "2px solid var(--border)", + background: "var(--background)", + outline: "none", + width: "100%", + padding: "0.5rem 0.75rem", + fontSize: "0.875rem", + fontFamily: "inherit", + resize: "vertical" as const, + minHeight: "80px", +}; + +export function QuizSection({ quiz, attempt, slug, lessonId }: Props) { + const [answers, setAnswers] = useState>( + attempt?.answers ?? {} + ); + const [pending, startTransition] = useTransition(); + + const allFilled = quiz.questions.every((q) => answers[q.id]?.trim()); + + if (attempt) { + return ( +
+
+ + Заполнено + +
+
+ {quiz.questions.map((q) => ( +
+

{q.text}

+
+ {attempt.answers[q.id] || "—"} +
+
+ ))} +
+
+ ); + } + + return ( +
+ {quiz.questions.map((q, idx) => ( +
+

+ {idx + 1}. {q.text} +

+