Mark lesson complete on homework submit; add copy button + styling to code blocks
This commit is contained in:
@@ -190,6 +190,33 @@
|
|||||||
.prose :where(ul ul) {
|
.prose :where(ul ul) {
|
||||||
list-style-type: circle;
|
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 (dark) */
|
||||||
.admin-sidebar {
|
.admin-sidebar {
|
||||||
|
|||||||
@@ -1,13 +1,61 @@
|
|||||||
"use client";
|
"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 { Node, mergeAttributes } from "@tiptap/core";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
import CodeBlock from "@tiptap/extension-code-block";
|
||||||
import Image from "@tiptap/extension-image";
|
import Image from "@tiptap/extension-image";
|
||||||
import Link from "@tiptap/extension-link";
|
import Link from "@tiptap/extension-link";
|
||||||
import Underline from "@tiptap/extension-underline";
|
import Underline from "@tiptap/extension-underline";
|
||||||
|
import { Copy, Check } from "lucide-react";
|
||||||
import { KinescopePlayer } from "@/components/player/kinescope-player";
|
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({
|
function KinescopeVideoStudentView({
|
||||||
node,
|
node,
|
||||||
extension,
|
extension,
|
||||||
@@ -56,7 +104,8 @@ const KinescopeVideoExtension = Node.create<{ poster?: string }>({
|
|||||||
export function LessonContent({ content, coverImage }: { content: object; coverImage?: string | null }) {
|
export function LessonContent({ content, coverImage }: { content: object; coverImage?: string | null }) {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit.configure({ codeBlock: false }),
|
||||||
|
CodeBlockWithCopy,
|
||||||
Underline,
|
Underline,
|
||||||
Image.configure({ inline: false }),
|
Image.configure({ inline: false }),
|
||||||
Link.configure({ openOnClick: true }),
|
Link.configure({ openOnClick: true }),
|
||||||
|
|||||||
@@ -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}/lessons/${lessonId}`);
|
||||||
|
revalidatePath(`/courses/${slug}`);
|
||||||
|
revalidatePath("/dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Quiz ──────────────────────────────────────────────────────────────────────
|
// ── Quiz ──────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user