142 lines
3.7 KiB
TypeScript
142 lines
3.7 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { BlockNoteView } from "@blocknote/mantine";
|
||
|
|
import { de } from "@blocknote/core/locales";
|
||
|
|
import { useCreateBlockNote } from "@blocknote/react";
|
||
|
|
import { useEffect } from "react";
|
||
|
|
|
||
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3002";
|
||
|
|
|
||
|
|
type NewsBlockNoteEditorProps = {
|
||
|
|
label?: string;
|
||
|
|
value: string;
|
||
|
|
placeholder?: string;
|
||
|
|
minHeightClassName?: string;
|
||
|
|
ensureLeadingHeading?: boolean;
|
||
|
|
onChange: (next: string) => void;
|
||
|
|
};
|
||
|
|
|
||
|
|
function normalizeHtml(html: string) {
|
||
|
|
const trimmed = html.trim();
|
||
|
|
if (!trimmed) return "";
|
||
|
|
if (trimmed === "<p></p>" || trimmed === "<p><br></p>") return "";
|
||
|
|
return trimmed;
|
||
|
|
}
|
||
|
|
|
||
|
|
function plainTextFromHtml(html: string) {
|
||
|
|
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function uploadEditorImage(file: File) {
|
||
|
|
const formData = new FormData();
|
||
|
|
formData.append("file", file);
|
||
|
|
|
||
|
|
const response = await fetch(`${API_URL}/api/uploads/image`, {
|
||
|
|
method: "POST",
|
||
|
|
credentials: "include",
|
||
|
|
body: formData,
|
||
|
|
});
|
||
|
|
|
||
|
|
const payload = (await response.json().catch(() => ({}))) as { url?: string; error?: string };
|
||
|
|
if (!response.ok || !payload.url) {
|
||
|
|
throw new Error(payload.error ?? "Bild-Upload fehlgeschlagen");
|
||
|
|
}
|
||
|
|
|
||
|
|
return payload.url;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function NewsBlockNoteEditor({
|
||
|
|
label,
|
||
|
|
value,
|
||
|
|
placeholder = "Hier den Beitrag schreiben...",
|
||
|
|
minHeightClassName = "min-h-[480px]",
|
||
|
|
ensureLeadingHeading = false,
|
||
|
|
onChange,
|
||
|
|
}: NewsBlockNoteEditorProps) {
|
||
|
|
const editor = useCreateBlockNote({
|
||
|
|
dictionary: {
|
||
|
|
...de,
|
||
|
|
placeholders: {
|
||
|
|
...(de as { placeholders?: Record<string, string> }).placeholders,
|
||
|
|
default: placeholder,
|
||
|
|
emptyDocument: placeholder,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
uploadFile: uploadEditorImage,
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const currentHtml = normalizeHtml(editor.blocksToHTMLLossy(editor.document));
|
||
|
|
const targetHtml = normalizeHtml(value || "");
|
||
|
|
if (currentHtml === targetHtml) return;
|
||
|
|
|
||
|
|
const starterBlocks = [
|
||
|
|
{
|
||
|
|
type: "heading",
|
||
|
|
props: { level: 1 },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
type: "paragraph",
|
||
|
|
},
|
||
|
|
] as never;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const blocks = targetHtml ? editor.tryParseHTMLToBlocks(targetHtml) : [];
|
||
|
|
if (blocks.length > 0) {
|
||
|
|
editor.replaceBlocks(editor.document, blocks);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fallbackText = plainTextFromHtml(targetHtml);
|
||
|
|
if (!fallbackText) {
|
||
|
|
editor.replaceBlocks(editor.document, ensureLeadingHeading ? starterBlocks : []);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
editor.replaceBlocks(editor.document, [
|
||
|
|
{
|
||
|
|
type: "paragraph",
|
||
|
|
content: fallbackText,
|
||
|
|
} as never,
|
||
|
|
]);
|
||
|
|
} catch {
|
||
|
|
const fallbackText = plainTextFromHtml(targetHtml);
|
||
|
|
if (!fallbackText) {
|
||
|
|
if (ensureLeadingHeading) {
|
||
|
|
editor.replaceBlocks(editor.document, starterBlocks);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
editor.replaceBlocks(editor.document, [
|
||
|
|
{
|
||
|
|
type: "paragraph",
|
||
|
|
content: fallbackText,
|
||
|
|
} as never,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}, [editor, ensureLeadingHeading, value]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
{label ? <label className="block text-sm font-medium text-gray-700 mb-1">{label}</label> : null}
|
||
|
|
<div
|
||
|
|
className={`rounded-md border border-gray-200 overflow-hidden bg-white ${minHeightClassName}`}
|
||
|
|
>
|
||
|
|
<BlockNoteView
|
||
|
|
editor={editor}
|
||
|
|
theme="light"
|
||
|
|
onChange={() => {
|
||
|
|
const html = normalizeHtml(editor.blocksToHTMLLossy(editor.document));
|
||
|
|
onChange(html);
|
||
|
|
}}
|
||
|
|
slashMenu
|
||
|
|
formattingToolbar
|
||
|
|
sideMenu
|
||
|
|
tableHandles
|
||
|
|
className="bg-white"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|