2012b0bab9
- Added default_language field to general settings state in SettingsPage - Replaced LanguageSelector component with simple select dropdown on settings page - Fixed public settings endpoint to read general_default_language from database - Language setting now properly saved when clicking Save Settings button - Setting is correctly used by gallery login page and legal pages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
5.2 KiB
TypeScript
191 lines
5.2 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Link from '@tiptap/extension-link';
|
|
import {
|
|
Bold,
|
|
Italic,
|
|
List,
|
|
ListOrdered,
|
|
Link as LinkIcon,
|
|
Heading1,
|
|
Heading2,
|
|
Undo,
|
|
Redo
|
|
} from 'lucide-react';
|
|
import { Button } from '../common';
|
|
|
|
interface CMSEditorProps {
|
|
content: string;
|
|
onChange: (content: string) => void;
|
|
}
|
|
|
|
export const CMSEditor: React.FC<CMSEditorProps> = ({ content, onChange }) => {
|
|
const [linkUrl, setLinkUrl] = useState('');
|
|
const [showLinkDialog, setShowLinkDialog] = useState(false);
|
|
|
|
const editor = useEditor({
|
|
extensions: [
|
|
StarterKit,
|
|
Link.configure({
|
|
openOnClick: false,
|
|
}),
|
|
],
|
|
content,
|
|
onUpdate: ({ editor }) => {
|
|
onChange(editor.getHTML());
|
|
},
|
|
});
|
|
|
|
// Update editor content when prop changes
|
|
React.useEffect(() => {
|
|
if (editor && content !== editor.getHTML()) {
|
|
editor.commands.setContent(content);
|
|
}
|
|
}, [content, editor]);
|
|
|
|
if (!editor) {
|
|
return null;
|
|
}
|
|
|
|
const addLink = () => {
|
|
if (linkUrl) {
|
|
editor.chain().focus().setLink({ href: linkUrl }).run();
|
|
setLinkUrl('');
|
|
setShowLinkDialog(false);
|
|
}
|
|
};
|
|
|
|
const MenuButton: React.FC<{
|
|
onClick: () => void;
|
|
active?: boolean;
|
|
children: React.ReactNode;
|
|
title: string;
|
|
}> = ({ onClick, active, children, title }) => (
|
|
<button
|
|
onClick={onClick}
|
|
className={`p-2 rounded hover:bg-neutral-100 ${
|
|
active ? 'bg-primary-100 text-primary-700' : 'text-neutral-700'
|
|
}`}
|
|
title={title}
|
|
type="button"
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<div className="border border-neutral-300 rounded-lg overflow-hidden">
|
|
{/* Toolbar */}
|
|
<div className="flex items-center gap-1 p-2 border-b border-neutral-200 bg-neutral-50 flex-wrap">
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
|
active={editor.isActive('heading', { level: 1 })}
|
|
title="Heading 1"
|
|
>
|
|
<Heading1 className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
active={editor.isActive('heading', { level: 2 })}
|
|
title="Heading 2"
|
|
>
|
|
<Heading2 className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<div className="w-px h-6 bg-neutral-300 mx-1" />
|
|
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
active={editor.isActive('bold')}
|
|
title="Bold"
|
|
>
|
|
<Bold className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
active={editor.isActive('italic')}
|
|
title="Italic"
|
|
>
|
|
<Italic className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<div className="w-px h-6 bg-neutral-300 mx-1" />
|
|
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
active={editor.isActive('bulletList')}
|
|
title="Bullet List"
|
|
>
|
|
<List className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
active={editor.isActive('orderedList')}
|
|
title="Ordered List"
|
|
>
|
|
<ListOrdered className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<div className="w-px h-6 bg-neutral-300 mx-1" />
|
|
|
|
<MenuButton
|
|
onClick={() => setShowLinkDialog(true)}
|
|
active={editor.isActive('link')}
|
|
title="Add Link"
|
|
>
|
|
<LinkIcon className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<div className="w-px h-6 bg-neutral-300 mx-1" />
|
|
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().undo().run()}
|
|
title="Undo"
|
|
>
|
|
<Undo className="w-4 h-4" />
|
|
</MenuButton>
|
|
|
|
<MenuButton
|
|
onClick={() => editor.chain().focus().redo().run()}
|
|
title="Redo"
|
|
>
|
|
<Redo className="w-4 h-4" />
|
|
</MenuButton>
|
|
</div>
|
|
|
|
{/* Link Dialog */}
|
|
{showLinkDialog && (
|
|
<div className="p-3 bg-primary-50 border-b border-primary-200 flex items-center gap-2">
|
|
<input
|
|
type="url"
|
|
value={linkUrl}
|
|
onChange={(e) => setLinkUrl(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && addLink()}
|
|
placeholder="Enter URL..."
|
|
className="flex-1 px-3 py-1 border border-primary-300 rounded-md focus:ring-2 focus:ring-primary-500"
|
|
autoFocus
|
|
/>
|
|
<Button size="sm" onClick={addLink}>Add Link</Button>
|
|
<Button size="sm" variant="outline" onClick={() => {
|
|
setShowLinkDialog(false);
|
|
setLinkUrl('');
|
|
}}>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Editor */}
|
|
<EditorContent
|
|
editor={editor}
|
|
className="min-h-[300px] p-4 prose prose-neutral max-w-none focus:outline-none"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
CMSEditor.displayName = 'CMSEditor'; |