From 7888a7598b518ce582fadba08d8b1acbf33ff923 Mon Sep 17 00:00:00 2001 From: Dmitriy Laukhin Date: Fri, 1 May 2026 13:26:30 +0000 Subject: [PATCH] Add coverImage poster to player, fix TipTap v3 editor reset, quiz admin preview - Add coverImage field to Lesson model (prisma) - Pass coverImage as poster prop to KinescopePlayer - Show quiz in read-only preview mode for admin on lesson page - Fix TipTap v3 editor reset on save: pass [lesson.id] as deps to useEditor to prevent setOptions() from reinitializing content on every re-render - Replace saveLesson Server Action call with fetch PATCH /api/admin/lessons/[id] to avoid Next.js 16 automatic RSC refresh after Server Actions - Simplify revalidatePath: only revalidate module page, not lesson editor page --- prisma/schema.prisma | 1 + .../[slug]/lessons/[lessonId]/page.tsx | 31 ++++++++++----- src/app/api/admin/lessons/[lessonId]/route.ts | 34 ++++++++++++++++ src/components/admin/lesson-editor.tsx | 39 +++++++++++++------ src/components/player/kinescope-player.tsx | 4 +- src/lib/actions/lesson-actions.ts | 35 +++++++++++------ 6 files changed, 111 insertions(+), 33 deletions(-) create mode 100644 src/app/api/admin/lessons/[lessonId]/route.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 26ae41e..6287836 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -130,6 +130,7 @@ model Lesson { title String content Json? kinescopeId String? + coverImage String? order Int @default(0) published Boolean @default(false) createdAt DateTime @default(now()) diff --git a/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx b/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx index f836f7a..9bb4a0c 100644 --- a/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx +++ b/src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx @@ -113,7 +113,7 @@ export default async function LessonPage({ params }: Props) { {/* Video */} {lesson.kinescopeId && (
- +
)} @@ -180,17 +180,30 @@ export default async function LessonPage({ params }: Props) { )} {/* Quiz */} - {lesson.quiz && !isAdmin && ( + {lesson.quiz && (

- Тест + Тест{isAdmin && (предпросмотр)}

- } : null} - slug={slug} - lessonId={lessonId} - /> + {isAdmin ? ( +
+ {lesson.quiz.questions.map((q, idx) => ( +
+

{idx + 1}. {q.text}

+
+ Поле для ответа студента +
+
+ ))} +
+ ) : ( + } : null} + slug={slug} + lessonId={lessonId} + /> + )}
)} diff --git a/src/app/api/admin/lessons/[lessonId]/route.ts b/src/app/api/admin/lessons/[lessonId]/route.ts new file mode 100644 index 0000000..8723e9b --- /dev/null +++ b/src/app/api/admin/lessons/[lessonId]/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { headers } from "next/headers"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function PATCH( + req: NextRequest, + { params }: { params: Promise<{ lessonId: string }> } +) { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session || session.user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { lessonId } = await params; + const body = await req.json() as { + title: string; + kinescopeId: string; + content: object; + published: boolean; + }; + + await prisma.lesson.update({ + where: { id: lessonId }, + data: { + title: body.title, + kinescopeId: body.kinescopeId || null, + content: body.content, + published: body.published, + }, + }); + + return NextResponse.json({ ok: true }); +} diff --git a/src/components/admin/lesson-editor.tsx b/src/components/admin/lesson-editor.tsx index b1c501f..e328a20 100644 --- a/src/components/admin/lesson-editor.tsx +++ b/src/components/admin/lesson-editor.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useTransition } from "react"; +import { useState, useCallback } from "react"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import Image from "@tiptap/extension-image"; @@ -9,7 +9,6 @@ import Underline from "@tiptap/extension-underline"; import Placeholder from "@tiptap/extension-placeholder"; import { Save, Eye, FileUp, ChevronLeft, ChevronRight } from "lucide-react"; import { useRouter } from "next/navigation"; -import { saveLesson } from "@/lib/actions/lesson-actions"; interface LessonData { id: string; @@ -47,7 +46,8 @@ export function LessonEditor({ const [importing, setImporting] = useState(false); const [importError, setImportError] = useState(null); const [saved, setSaved] = useState(false); - const [pending, startTransition] = useTransition(); + const [saveError, setSaveError] = useState(null); + const [pending, setPending] = useState(false); const inputStyle = { border: "2px solid var(--border)", @@ -73,7 +73,7 @@ export function LessonEditor({ class: "prose prose-slate max-w-none min-h-[300px] focus:outline-none p-4", }, }, - }); + }, [lesson.id]); const uploadImage = useCallback(async () => { const input = document.createElement("input"); @@ -133,18 +133,27 @@ export function LessonEditor({ } }, [editor]); - function handleSave() { + async function handleSave() { if (!editor) return; - startTransition(async () => { - await saveLesson(lesson.id, courseId, moduleId, { - title, - kinescopeId, - content: editor.getJSON(), - published, + setPending(true); + setSaveError(null); + try { + const res = await fetch(`/api/admin/lessons/${lesson.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title, kinescopeId, content: editor.getJSON(), published }), }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error((data as { error?: string }).error ?? `HTTP ${res.status}`); + } setSaved(true); setTimeout(() => setSaved(false), 2000); - }); + } catch (err) { + setSaveError(err instanceof Error ? err.message : "Ошибка сохранения"); + } finally { + setPending(false); + } } function navigateTo(lessonId: string) { @@ -266,6 +275,12 @@ export function LessonEditor({

)} + {saveError && ( +

+ Ошибка сохранения: {saveError} +

+ )} + {/* Title */}