From 6c197e1fc51be7cf59186705f64667f89d21eb1f Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 21 Aug 2026 13:51:56 +0200 Subject: [PATCH] feat(UI-Overhaul-Phase3): Wiki WYSIWYG Editor mit Tiptap - WikiEditor.tsx: Complete rebuild with Tiptap WYSIWYG editor - Notion-style floating toolbar with bold/italic/underline/strike - Headings (H1/H2/H3), bullet/ordered lists, blockquote, code blocks - Link and image insertion - Text alignment (left/center/right) - Undo/redo support - HTML-to-Markdown conversion for backend storage - Markdown-to-HTML conversion for editor initialization - Wiki.tsx: View/Edit mode toggle - View mode: Rendered Markdown (ReactMarkdown) - Edit mode: WYSIWYG editor (Tiptap) - Inline save button in edit mode - wikiMode resets to view when selecting new article - handleInlineSave saves article content directly tsc clean --- frontend/src/components/wiki/WikiEditor.tsx | 243 +++++++++++++++----- frontend/src/pages/Wiki.tsx | 58 ++++- 2 files changed, 242 insertions(+), 59 deletions(-) diff --git a/frontend/src/components/wiki/WikiEditor.tsx b/frontend/src/components/wiki/WikiEditor.tsx index 677f86d..5fea695 100644 --- a/frontend/src/components/wiki/WikiEditor.tsx +++ b/frontend/src/components/wiki/WikiEditor.tsx @@ -1,16 +1,27 @@ /** - * WikiEditor — Markdown editor with live preview. + * WikiEditor — WYSIWYG editor using Tiptap (Notion-style). * - * Uses a textarea for Markdown input and react-markdown + remark-gfm for - * the live preview pane. Content is stored as Markdown in the backend. + * Features: Floating toolbar, Markdown export for backend storage, + * bold/italic/underline/strike, headings, lists, links, code blocks, images. */ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; +import { useEditor, EditorContent, Editor } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Underline from '@tiptap/extension-underline'; +import Link from '@tiptap/extension-link'; +import Image from '@tiptap/extension-image'; +import Placeholder from '@tiptap/extension-placeholder'; +import TextAlign from '@tiptap/extension-text-align'; +import { TextStyle } from "@tiptap/extension-text-style"; +import Color from '@tiptap/extension-color'; import clsx from 'clsx'; -import { Eye, PencilLine, Columns2 } from 'lucide-react'; +import { + Bold, Italic, Underline as UnderlineIcon, Strikethrough, Heading1, Heading2, Heading3, + List, ListOrdered, Quote, Code, Link as LinkIcon, Image as ImageIcon, + AlignLeft, AlignCenter, AlignRight, Undo, Redo, +} from 'lucide-react'; export interface WikiEditorProps { value: string; @@ -19,61 +30,187 @@ export interface WikiEditorProps { minHeight?: string; } -type EditorMode = 'write' | 'preview' | 'split'; - -export function WikiEditor({ value, onChange, placeholder, minHeight = 'min-h-72' }: WikiEditorProps) { - const { t } = useTranslation(); - const [mode, setMode] = useState('split'); - - const preview = useMemo( - () => ( -
- {value || t('wiki.editor.emptyPreview')} -
- ), - [value, t] - ); - - const modeButton = (m: EditorMode, label: string, Icon: React.ComponentType<{ className?: string }>) => ( +function ToolbarButton({ + onClick, isActive, disabled, icon: Icon, label, +}: { + onClick: () => void; + isActive?: boolean; + disabled?: boolean; + icon: React.ComponentType<{ className?: string }>; + label: string; +}) { + return ( ); +} + +function Toolbar({ editor }: { editor: Editor | null }) { + const { t } = useTranslation(); + if (!editor) return null; + + const setLink = useCallback(() => { + const previousUrl = editor.getAttributes('link').href; + const url = window.prompt('URL', previousUrl); + if (url === null) return; + if (url === '') { + editor.chain().focus().extendMarkRange('link').unsetLink().run(); + return; + } + editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); + }, [editor]); + + const addImage = useCallback(() => { + const url = window.prompt('Bild URL'); + if (url) editor.chain().focus().setImage({ src: url }).run(); + }, [editor]); return ( -
-
- {modeButton('write', t('editor.write'), PencilLine)} - {modeButton('preview', t('editor.preview'), Eye)} - {modeButton('split', t('editor.split'), Columns2)} -
-
- {(mode === 'write' || mode === 'split') && ( -