Files
lms-sb/src/components/student/lesson-content.tsx
T

127 lines
3.9 KiB
TypeScript

"use client";
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 () => {
// WYSIWYG: копируем содержимое блока ровно как показано. Для Obsidian-сниппетов
// (dataview/query) забор ```...``` хранится прямо в содержимом → копируется целиком.
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,
}: {
node: { attrs: Record<string, unknown> };
extension: { options: { poster?: string } };
}) {
const videoId = node.attrs.videoId as string;
if (!videoId) return null;
return (
<NodeViewWrapper>
<div className="my-6">
<KinescopePlayer videoId={videoId} poster={extension.options.poster} />
</div>
</NodeViewWrapper>
);
}
const KinescopeVideoExtension = Node.create<{ poster?: string }>({
name: "kinescopeVideo",
group: "block",
atom: true,
addOptions() {
return { poster: undefined };
},
addAttributes() {
return { videoId: { default: "" } };
},
parseHTML() {
return [{ tag: "div[data-kinescope-video]" }];
},
renderHTML({ HTMLAttributes }) {
return ["div", mergeAttributes({ "data-kinescope-video": "" }, HTMLAttributes)];
},
addNodeView() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ReactNodeViewRenderer(KinescopeVideoStudentView as any);
},
});
export function LessonContent({ content, coverImage }: { content: object; coverImage?: string | null }) {
const editor = useEditor({
extensions: [
StarterKit.configure({ codeBlock: false }),
CodeBlockWithCopy,
Underline,
Image.configure({ inline: false }),
Link.configure({ openOnClick: true }),
KinescopeVideoExtension.configure({ poster: coverImage ?? undefined }),
],
content,
editable: false,
editorProps: {
attributes: {
class: "prose prose-slate max-w-none focus:outline-none",
},
},
});
return <EditorContent editor={editor} />;
}