diff --git a/src/app/globals.css b/src/app/globals.css index 61871a0..bfa110f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -190,6 +190,33 @@ .prose :where(ul ul) { list-style-type: circle; } +/* Код-блоки урока (с кнопкой копирования) */ +.prose pre { + background: var(--color-surface, #f3f3ee); + border: 1.5px solid var(--border); + padding: 2.5rem 1rem 1rem; /* верхний отступ — под кнопку «Копировать» */ + margin: 0; + overflow-x: auto; + font-family: var(--font-mono); + font-size: 0.85em; + line-height: 1.55; +} +.prose pre code { + background: none; + border: none; + padding: 0; + font-family: inherit; + font-size: inherit; + white-space: pre; +} +/* Инлайновый код */ +.prose :not(pre) > code { + background: var(--color-surface, #f3f3ee); + border: 1px solid var(--border); + padding: 0.1em 0.35em; + font-family: var(--font-mono); + font-size: 0.9em; +} /* Admin sidebar (dark) */ .admin-sidebar { diff --git a/src/components/student/lesson-content.tsx b/src/components/student/lesson-content.tsx index ef91613..82faa5e 100644 --- a/src/components/student/lesson-content.tsx +++ b/src/components/student/lesson-content.tsx @@ -1,13 +1,61 @@ "use client"; -import { useEditor, EditorContent, NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"; +import { useState } from "react"; +import { useEditor, EditorContent, NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from "@tiptap/react"; import { Node, mergeAttributes } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; +import CodeBlock from "@tiptap/extension-code-block"; import Image from "@tiptap/extension-image"; import Link from "@tiptap/extension-link"; import Underline from "@tiptap/extension-underline"; +import { Copy, Check } from "lucide-react"; import { KinescopePlayer } from "@/components/player/kinescope-player"; +function CodeBlockStudentView({ node }: { node: { textContent: string } }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(node.textContent); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + // clipboard недоступен (нет https / нет прав) — тихо игнорируем + } + }; + + return ( + + +
+         as="code" />
+      
+
+ ); +} + +const CodeBlockWithCopy = CodeBlock.extend({ + addNodeView() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ReactNodeViewRenderer(CodeBlockStudentView as any); + }, +}); + function KinescopeVideoStudentView({ node, extension, @@ -56,7 +104,8 @@ const KinescopeVideoExtension = Node.create<{ poster?: string }>({ export function LessonContent({ content, coverImage }: { content: object; coverImage?: string | null }) { const editor = useEditor({ extensions: [ - StarterKit, + StarterKit.configure({ codeBlock: false }), + CodeBlockWithCopy, Underline, Image.configure({ inline: false }), Link.configure({ openOnClick: true }), diff --git a/src/lib/actions/student-actions.ts b/src/lib/actions/student-actions.ts index 53594cc..7f7d109 100644 --- a/src/lib/actions/student-actions.ts +++ b/src/lib/actions/student-actions.ts @@ -96,7 +96,16 @@ export async function submitHomework( } } + // Отметить урок пройденным по факту сдачи ДЗ (идемпотентно) + await prisma.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"); } // ── Quiz ──────────────────────────────────────────────────────────────────────