132 lines
7.4 KiB
TypeScript
132 lines
7.4 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import TiptapLink from '@tiptap/extension-link';
|
|
import { gdprApi } from '../../services/api';
|
|
import Button from '../../components/ui/Button';
|
|
import Card from '../../components/ui/Card';
|
|
import { ArrowLeft, Save, Eye, Bold, Italic, List, ListOrdered, Heading1, Heading2, Heading3, Link as LinkIcon, Undo, Redo, Type } from 'lucide-react';
|
|
import { Link as RouterLink } from 'react-router-dom';
|
|
|
|
const DEFAULT_IMPRINT = `<h1>Impressum</h1>
|
|
|
|
<h2>Angaben gemäß § 5 TMG</h2>
|
|
|
|
<p>Hacker-Net Telekommunikation<br>
|
|
Stefan Hacker<br>
|
|
Am Wunderburgpark 5b<br>
|
|
26135 Oldenburg</p>
|
|
|
|
<h2>Kontakt</h2>
|
|
|
|
<p>Telefon: [Ihre Telefonnummer]<br>
|
|
E-Mail: info@hacker-net.de</p>
|
|
|
|
<h2>Umsatzsteuer-ID</h2>
|
|
|
|
<p>Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:<br>
|
|
[Ihre USt-IdNr.]</p>
|
|
|
|
<h2>Berufsbezeichnung und berufsrechtliche Regelungen</h2>
|
|
|
|
<p>Berufsbezeichnung: Telekommunikationsdienstleister<br>
|
|
Zuständige Kammer: [IHK Oldenburg]<br>
|
|
Verliehen in: Deutschland</p>
|
|
|
|
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
|
|
|
|
<p>Stefan Hacker<br>
|
|
Am Wunderburgpark 5b<br>
|
|
26135 Oldenburg</p>
|
|
|
|
<h2>EU-Streitschlichtung</h2>
|
|
|
|
<p>Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
|
|
<a href="https://ec.europa.eu/consumers/odr/" target="_blank">https://ec.europa.eu/consumers/odr/</a>.<br>
|
|
Unsere E-Mail-Adresse finden Sie oben im Impressum.</p>
|
|
|
|
<h2>Verbraucherstreitbeilegung / Universalschlichtungsstelle</h2>
|
|
|
|
<p>Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>`;
|
|
|
|
function MenuBar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
|
if (!editor) return null;
|
|
const setLink = useCallback(() => {
|
|
const previousUrl = editor.getAttributes('link').href;
|
|
const url = window.prompt('URL eingeben:', 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 btnClass = (active: boolean) => `p-1.5 rounded hover:bg-gray-200 transition-colors ${active ? 'bg-gray-200 text-blue-600' : 'text-gray-600'}`;
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-1 p-2 border-b bg-gray-50">
|
|
<button type="button" onClick={() => editor.chain().focus().toggleBold().run()} className={btnClass(editor.isActive('bold'))} title="Fett"><Bold className="w-4 h-4" /></button>
|
|
<button type="button" onClick={() => editor.chain().focus().toggleItalic().run()} className={btnClass(editor.isActive('italic'))} title="Kursiv"><Italic className="w-4 h-4" /></button>
|
|
<div className="w-px h-5 bg-gray-300 mx-1" />
|
|
<button type="button" onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} className={btnClass(editor.isActive('heading', { level: 1 }))} title="Überschrift 1"><Heading1 className="w-4 h-4" /></button>
|
|
<button type="button" onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={btnClass(editor.isActive('heading', { level: 2 }))} title="Überschrift 2"><Heading2 className="w-4 h-4" /></button>
|
|
<button type="button" onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} className={btnClass(editor.isActive('heading', { level: 3 }))} title="Überschrift 3"><Heading3 className="w-4 h-4" /></button>
|
|
<button type="button" onClick={() => editor.chain().focus().setParagraph().run()} className={btnClass(editor.isActive('paragraph'))} title="Absatz"><Type className="w-4 h-4" /></button>
|
|
<div className="w-px h-5 bg-gray-300 mx-1" />
|
|
<button type="button" onClick={() => editor.chain().focus().toggleBulletList().run()} className={btnClass(editor.isActive('bulletList'))} title="Aufzählung"><List className="w-4 h-4" /></button>
|
|
<button type="button" onClick={() => editor.chain().focus().toggleOrderedList().run()} className={btnClass(editor.isActive('orderedList'))} title="Nummerierung"><ListOrdered className="w-4 h-4" /></button>
|
|
<div className="w-px h-5 bg-gray-300 mx-1" />
|
|
<button type="button" onClick={setLink} className={btnClass(editor.isActive('link'))} title="Link"><LinkIcon className="w-4 h-4" /></button>
|
|
<div className="w-px h-5 bg-gray-300 mx-1" />
|
|
<button type="button" onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Rückgängig"><Undo className="w-4 h-4" /></button>
|
|
<button type="button" onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Wiederherstellen"><Redo className="w-4 h-4" /></button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ImprintEditor() {
|
|
const queryClient = useQueryClient();
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
const [saved, setSaved] = useState(false);
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['imprint'],
|
|
queryFn: () => gdprApi.getImprint(),
|
|
});
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: (html: string) => gdprApi.updateImprint(html),
|
|
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['imprint'] }); setSaved(true); setTimeout(() => setSaved(false), 2000); },
|
|
});
|
|
|
|
const initialContent = data?.data?.html || DEFAULT_IMPRINT;
|
|
const editor = useEditor({
|
|
extensions: [StarterKit, TiptapLink.configure({ openOnClick: false, HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer' } })],
|
|
content: initialContent,
|
|
editorProps: { attributes: { class: 'prose prose-sm max-w-none p-4 min-h-[400px] focus:outline-none' } },
|
|
}, [initialContent]);
|
|
|
|
const handleSave = () => { if (editor) saveMutation.mutate(editor.getHTML()); };
|
|
const handleReset = () => { if (confirm('Auf Standardtext zurücksetzen?')) editor?.commands.setContent(DEFAULT_IMPRINT); };
|
|
|
|
if (isLoading) return <div className="text-center py-8 text-gray-500">Laden...</div>;
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<RouterLink to="/settings"><Button variant="ghost" size="sm"><ArrowLeft className="w-4 h-4" /></Button></RouterLink>
|
|
<h1 className="text-2xl font-bold flex-1">Impressum bearbeiten</h1>
|
|
<Button variant="ghost" size="sm" onClick={handleReset}>Standardtext laden</Button>
|
|
<Button variant="secondary" onClick={() => setShowPreview(!showPreview)}><Eye className="w-4 h-4 mr-2" />{showPreview ? 'Editor' : 'Vorschau'}</Button>
|
|
<Button onClick={handleSave} disabled={saveMutation.isPending}><Save className="w-4 h-4 mr-2" />{saved ? 'Gespeichert!' : saveMutation.isPending ? 'Speichern...' : 'Speichern'}</Button>
|
|
</div>
|
|
{showPreview ? (
|
|
<Card><div className="prose prose-sm max-w-none p-4" dangerouslySetInnerHTML={{ __html: editor?.getHTML() || '' }} /></Card>
|
|
) : (
|
|
<div className="border rounded-lg bg-white overflow-hidden">
|
|
<MenuBar editor={editor} />
|
|
<EditorContent editor={editor} />
|
|
</div>
|
|
)}
|
|
{saveMutation.isError && <div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">Fehler beim Speichern.</div>}
|
|
</div>
|
|
);
|
|
}
|