'use client'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EditorContent, useEditor, type JSONContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import Underline from '@tiptap/extension-underline'; import Link from '@tiptap/extension-link'; import { TextStyle } from '@tiptap/extension-text-style'; import Color from '@tiptap/extension-color'; import Highlight from '@tiptap/extension-highlight'; import { Bold, Italic, Underline as UnderlineIcon, List, ListOrdered, Link as LinkIcon, Highlighter, Type, Save, RefreshCw } from 'lucide-react'; import { FontFamily, type AllowedFontFamily } from '@/lib/tiptap/fontFamily'; const EMPTY_DOC: JSONContent = { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }], }; type PageListItem = { id: number; key: string; translations: Array<{ locale: string; updatedAt: string; title: string | null; slug: string | null }>; }; export default function ContentManager() { const [pages, setPages] = useState([]); const [selectedKey, setSelectedKey] = useState('privacy-policy'); const [selectedLocale, setSelectedLocale] = useState('de'); const [title, setTitle] = useState(''); const [slug, setSlug] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(''); const [fontFamily, setFontFamily] = useState(''); const [color, setColor] = useState('#111827'); const extensions = useMemo( () => [ StarterKit, Underline, Link.configure({ openOnClick: false, HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' }, }), TextStyle, FontFamily, Color, Highlight, ], [], ); const editor = useEditor({ extensions, content: EMPTY_DOC, editorProps: { attributes: { class: 'prose prose-stone max-w-none focus:outline-none min-h-[320px] p-4 bg-white rounded-xl border border-stone-200', }, }, }); const sessionHeaders = () => { const sessionToken = sessionStorage.getItem('admin_session_token') || ''; return { 'x-admin-request': 'true', 'x-session-token': sessionToken, 'Content-Type': 'application/json', }; }; const loadPages = useCallback(async () => { setError(''); try { setIsLoading(true); const res = await fetch('/api/content/pages', { headers: sessionHeaders() }); const data = await res.json(); if (!res.ok) throw new Error(data?.error || 'Failed to load content pages'); setPages(data.pages || []); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to load content pages'); } finally { setIsLoading(false); } }, []); const loadSelected = useCallback(async () => { if (!editor) return; setError(''); try { setIsLoading(true); const res = await fetch(`/api/content/page?key=${encodeURIComponent(selectedKey)}&locale=${encodeURIComponent(selectedLocale)}`); const data = await res.json(); const translation = data?.content; const nextTitle = (translation?.title as string | undefined) || ''; const nextSlug = (translation?.slug as string | undefined) || ''; const nextDoc = (translation?.content as JSONContent | undefined) || EMPTY_DOC; setTitle(nextTitle); setSlug(nextSlug); editor.commands.setContent(nextDoc); setFontFamily(''); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to load content'); } finally { setIsLoading(false); } }, [editor, selectedKey, selectedLocale]); useEffect(() => { loadPages(); }, [loadPages]); useEffect(() => { loadSelected(); }, [loadSelected]); const handleSave = async () => { if (!editor) return; setError(''); try { setIsSaving(true); const content = editor.getJSON(); const res = await fetch('/api/content/pages', { method: 'POST', headers: sessionHeaders(), body: JSON.stringify({ key: selectedKey, locale: selectedLocale, title: title || null, slug: slug || null, content, }), }); const data = await res.json(); if (!res.ok) throw new Error(data?.error || 'Failed to save content'); await loadPages(); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to save content'); } finally { setIsSaving(false); } }; const localeOptions = ['en', 'de']; const fontOptions: Array<{ label: string; value: AllowedFontFamily | '' }> = [ { label: 'Default', value: '' }, { label: 'Inter', value: 'Inter' }, { label: 'Sans', value: 'ui-sans-serif' }, { label: 'Serif', value: 'ui-serif' }, { label: 'Mono', value: 'ui-monospace' }, ]; const selectedInfo = useMemo(() => { const page = pages.find((p) => p.key === selectedKey); const tr = page?.translations?.find((t) => t.locale === selectedLocale); return tr; }, [pages, selectedKey, selectedLocale]); return (

Content Manager

Edit texts/pages with rich formatting (bold, underline, links, highlights).

{error && (
{error}
)}
Last updated:{' '} {selectedInfo?.updatedAt ? new Date(selectedInfo.updatedAt).toLocaleString() : '—'}
setTitle(e.target.value)} className="w-full px-3 py-2 bg-white border border-stone-200 rounded-lg text-stone-900 focus:outline-none focus:ring-2 focus:ring-stone-300" placeholder="Page title" />
setSlug(e.target.value)} className="w-full px-3 py-2 bg-white border border-stone-200 rounded-lg text-stone-900 focus:outline-none focus:ring-2 focus:ring-stone-300" placeholder="privacy-policy" />
Content
{isLoading ? (
Loading…
) : ( <> {editor && (
{ const next = e.target.value; setColor(next); editor.chain().focus().setColor(next).run(); }} className="w-10 h-10 p-1 bg-white border border-stone-200 rounded-lg" title="Text color" />
)} )}

Tip: Use bold/underline, links, lists, headings. (Email-safe rendering is handled separately.)

); }