"use client"; import { useState, useCallback } from "react"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import Image from "@tiptap/extension-image"; import Link from "@tiptap/extension-link"; 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"; interface LessonData { id: string; title: string; kinescopeId: string; content: object; published: boolean; } interface SiblingLesson { id: string; title: string; moduleId: string; } export function LessonEditor({ lesson, courseId, courseSlug, prevLesson, nextLesson, }: { lesson: LessonData; courseId: string; courseSlug: string; prevLesson?: SiblingLesson | null; nextLesson?: SiblingLesson | null; }) { const router = useRouter(); const [title, setTitle] = useState(lesson.title); const [kinescopeId, setKinescopeId] = useState(lesson.kinescopeId); const [published, setPublished] = useState(lesson.published); const [uploading, setUploading] = useState(false); const [importing, setImporting] = useState(false); const [importError, setImportError] = useState(null); const [saved, setSaved] = useState(false); const [saveError, setSaveError] = useState(null); const [pending, setPending] = useState(false); const inputStyle = { border: "2px solid var(--border)", background: "var(--background)", outline: "none", width: "100%", padding: "0.5rem 0.75rem", fontSize: "16px", fontFamily: "inherit", } as React.CSSProperties; const editor = useEditor({ extensions: [ StarterKit, Underline, Image.configure({ inline: false }), Link.configure({ openOnClick: false }), Placeholder.configure({ placeholder: "Начните писать текст урока..." }), ], content: Object.keys(lesson.content).length ? lesson.content : undefined, editorProps: { attributes: { 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"); input.type = "file"; input.accept = "image/*"; input.onchange = async () => { const file = input.files?.[0]; if (!file || !editor) return; setUploading(true); const fd = new FormData(); fd.append("file", file); const res = await fetch("/api/admin/upload", { method: "POST", body: fd }); const data = await res.json(); if (data.url) editor.chain().focus().setImage({ src: data.url }).run(); setUploading(false); }; input.click(); }, [editor]); const importMd = useCallback(() => { const input = document.createElement("input"); input.type = "file"; input.accept = ".md"; input.onchange = async () => { const file = input.files?.[0]; if (!file || !editor) return; setImporting(true); setImportError(null); try { const fd = new FormData(); fd.append("file", file); const res = await fetch("/api/admin/import-md", { method: "POST", body: fd }); if (!res.ok) throw new Error("Ошибка импорта"); const data = await res.json(); if (data.title) setTitle(data.title); if (data.kinescopeId) setKinescopeId(data.kinescopeId); if (data.published !== null) setPublished(data.published); if (data.content) editor.commands.setContent(data.content); } catch { setImportError("Не удалось импортировать файл"); } finally { setImporting(false); } }; input.click(); }, [editor]); const addLink = useCallback(() => { if (!editor) return; const prev = editor.getAttributes("link").href as string | undefined; const url = window.prompt("Ссылка:", prev ?? "https://"); if (url === null) return; if (url === "") { editor.chain().focus().unsetLink().run(); } else { editor.chain().focus().setLink({ href: url, target: "_blank" }).run(); } }, [editor]); async function handleSave() { if (!editor) return; 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(target: SiblingLesson) { router.push(`/admin/courses/${courseId}/modules/${target.moduleId}/lessons/${target.id}`); } return (
{/* Header controls */}
{/* Left: published toggle + prev/next */}
{/* Prev / Next navigation */}
{/* Right: Import / Preview / Save */}
Просмотр
{importError && (

{importError}

)} {saveError && (

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

)} {/* Title */}
setTitle(e.target.value)} style={{ ...inputStyle, fontSize: "1.1rem", fontWeight: "700" }} onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} />
{/* Kinescope ID */}
setKinescopeId(e.target.value)} placeholder="Оставьте пустым если видео нет" style={{ ...inputStyle, fontFamily: "var(--font-mono)" }} onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} />
{/* TipTap Editor */}
{/* Toolbar */}
{/* Text style */} editor?.chain().focus().toggleBold().run()} active={editor?.isActive("bold")}>Ж editor?.chain().focus().toggleItalic().run()} active={editor?.isActive("italic")}>К editor?.chain().focus().toggleUnderline().run()} active={editor?.isActive("underline")}>Ч editor?.chain().focus().toggleStrike().run()} active={editor?.isActive("strike")}>З editor?.chain().focus().toggleCode().run()} active={editor?.isActive("code")}>`code` {/* Headings */} editor?.chain().focus().toggleHeading({ level: 1 }).run()} active={editor?.isActive("heading", { level: 1 })}>H1 editor?.chain().focus().toggleHeading({ level: 2 }).run()} active={editor?.isActive("heading", { level: 2 })}>H2 editor?.chain().focus().toggleHeading({ level: 3 }).run()} active={editor?.isActive("heading", { level: 3 })}>H3 {/* Lists & blocks */} editor?.chain().focus().toggleBulletList().run()} active={editor?.isActive("bulletList")}>• Список editor?.chain().focus().toggleOrderedList().run()} active={editor?.isActive("orderedList")}>1. Список editor?.chain().focus().toggleBlockquote().run()} active={editor?.isActive("blockquote")}>“” Цитата editor?.chain().focus().toggleCodeBlock().run()} active={editor?.isActive("codeBlock")}>{''} Код editor?.chain().focus().setHorizontalRule().run()}>── Разделитель {/* Link & image */} 🔗 Ссылка {uploading ? "Загрузка..." : "🖼 Фото"} {/* History */} editor?.chain().focus().undo().run()}>↩ Отменить editor?.chain().focus().redo().run()}>↪ Повторить
{/* Editor content */}
); } function Sep() { return
; } function ToolBtn({ onClick, active, disabled, children, }: { onClick: () => void; active?: boolean; disabled?: boolean; children: React.ReactNode; }) { return ( ); }