Mark lesson complete on homework submit; add copy button + styling to code blocks

This commit is contained in:
2026-06-03 14:58:30 +05:00
parent adc1d6fc06
commit cf942d223e
3 changed files with 87 additions and 2 deletions
+27
View File
@@ -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 {
+51 -2
View File
@@ -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 (
<NodeViewWrapper className="relative group my-4">
<button
type="button"
onClick={handleCopy}
contentEditable={false}
aria-label="Скопировать код"
className="absolute top-2 right-2 z-10 flex items-center gap-1 px-2 py-1 text-xs select-none transition-opacity"
style={{
border: "1.5px solid var(--border)",
background: "var(--background)",
color: copied ? "var(--foreground)" : "var(--muted-foreground)",
fontFamily: "var(--font-mono, monospace)",
}}
>
{copied ? <Check size={13} strokeWidth={3} /> : <Copy size={13} />}
{copied ? "Скопировано" : "Копировать"}
</button>
<pre>
<NodeViewContent<"code"> as="code" />
</pre>
</NodeViewWrapper>
);
}
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 }),
+9
View File
@@ -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 ──────────────────────────────────────────────────────────────────────