'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import MDEditor, { commands } from '@uiw/react-md-editor'; import rehypeSanitize from 'rehype-sanitize'; import type { WidgetProps } from '../widget-registry'; import { updateWidgetConfig } from '@/lib/dashboard-api'; import { NoteCheckbox, toggleTaskLine } from './note-task-list'; const DEBOUNCE_MS = 1500; // quick-260916-iex: Aufgabenlisten sind in der Ansicht direkt abhakbar — // `components.input` ersetzt das per rehypeSanitize deaktivierte `input` // durch NoteCheckbox (siehe note-task-list.tsx). Stabile Konstante ausserhalb // der Komponente, damit react-markdown nicht bei jedem Render neu diffed. // Typ direkt von MDEditor abgeleitet statt aus dem transitiven Paket // `@uiw/react-markdown-preview` importiert (das ist keine direkte // Dependency von apps/web und daher unter pnpms strikter Isolation nicht // von hier aus aufloesbar). type PreviewOptions = NonNullable< React.ComponentProps['previewOptions'] >; const PREVIEW_OPTIONS: PreviewOptions = { rehypePlugins: [[rehypeSanitize]], components: { input: NoteCheckbox }, }; const NOTE_COMMANDS = [ commands.bold, commands.italic, commands.strikethrough, commands.divider, commands.unorderedListCommand, commands.checkedListCommand, commands.divider, commands.link, commands.code, ]; export function NoteWidget({ instanceId, config }: WidgetProps) { const t = useTranslations('widgets'); const [title, setTitle] = useState( (config.title as string) || t('note.defaultTitle'), ); const [content, setContent] = useState( (config.content as string) || '', ); const [isEditing, setIsEditing] = useState(false); const [saveError, setSaveError] = useState(false); const timerRef = useRef | undefined>(undefined); const abortRef = useRef(undefined); useEffect(() => { return () => { clearTimeout(timerRef.current); abortRef.current?.abort(); }; }, []); const save = useCallback( async (newContent: string, newTitle: string) => { abortRef.current?.abort(); abortRef.current = new AbortController(); try { await updateWidgetConfig(instanceId, { content: newContent, title: newTitle }, abortRef.current.signal); setSaveError(false); } catch (err: unknown) { if (err instanceof DOMException && err.name === 'AbortError') return; setSaveError(true); } }, [instanceId], ); const scheduleSave = useCallback( (newContent: string, newTitle: string) => { clearTimeout(timerRef.current); timerRef.current = setTimeout(() => save(newContent, newTitle), DEBOUNCE_MS); }, [save], ); const handleContentChange = useCallback( (val?: string) => { const newContent = val ?? ''; setContent(newContent); scheduleSave(newContent, title); }, [scheduleSave, title], ); const handleTitleChange = useCallback( (e: React.ChangeEvent) => { const newTitle = e.target.value; setTitle(newTitle); scheduleSave(content, newTitle); }, [scheduleSave, content], ); // quick-260916-iex: Klick auf ein Aufgaben-Kaestchen in der Ansicht kippt // genau diese Zeile und speichert sofort (kein Entprellen — ein Klick ist // eine abgeschlossene Handlung). Ein noch laufender Entprell-Timer aus dem // Tippen wird verworfen, damit er den ungekippten Text nicht nachschiebt. const handlePreviewClick = useCallback( (event: React.MouseEvent) => { if (isEditing) return; const target = event.target; if (!(target instanceof HTMLInputElement) || target.type !== 'checkbox') { return; } const boxes = Array.from( event.currentTarget.querySelectorAll( 'input[type="checkbox"]', ), ); const index = boxes.indexOf(target); const next = toggleTaskLine(content, index); if (next === content) return; setContent(next); clearTimeout(timerRef.current); void save(next, title); }, [isEditing, content, title, save], ); return (
{/* Header: title + pencil toggle */}
{saveError && ( )}
{/* Content: preview or editor */}
); }