mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 04:31:28 +02:00
- Add i18n support to AddSmartPlugModal.tsx - Add i18n support to ConfigureAmsSlotModal.tsx - Add i18n support to RichTextEditor.tsx - Add i18n support to ExternalLinksSettings.tsx - Replace all hardcoded English strings with t() calls Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
194 lines
5.5 KiB
TypeScript
194 lines
5.5 KiB
TypeScript
import { useEditor, EditorContent } from '@tiptap/react';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Link from '@tiptap/extension-link';
|
|
import Underline from '@tiptap/extension-underline';
|
|
import TextAlign from '@tiptap/extension-text-align';
|
|
import { TextStyle } from '@tiptap/extension-text-style';
|
|
import Color from '@tiptap/extension-color';
|
|
import Image from '@tiptap/extension-image';
|
|
import {
|
|
Bold,
|
|
Italic,
|
|
Underline as UnderlineIcon,
|
|
List,
|
|
ListOrdered,
|
|
AlignLeft,
|
|
AlignCenter,
|
|
AlignRight,
|
|
Link as LinkIcon,
|
|
Unlink,
|
|
} from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface RichTextEditorProps {
|
|
content: string;
|
|
onChange: (html: string) => void;
|
|
placeholder?: string;
|
|
}
|
|
|
|
export function RichTextEditor({ content, onChange, placeholder }: RichTextEditorProps) {
|
|
const { t } = useTranslation();
|
|
const editor = useEditor({
|
|
extensions: [
|
|
StarterKit.configure({
|
|
heading: false,
|
|
codeBlock: false,
|
|
code: false,
|
|
}),
|
|
Underline,
|
|
Link.configure({
|
|
openOnClick: false,
|
|
HTMLAttributes: {
|
|
target: '_blank',
|
|
rel: 'noopener noreferrer',
|
|
},
|
|
}),
|
|
TextAlign.configure({
|
|
types: ['paragraph'],
|
|
}),
|
|
TextStyle,
|
|
Color,
|
|
Image.configure({
|
|
HTMLAttributes: {
|
|
style: 'max-width: 100%; height: auto;',
|
|
},
|
|
}),
|
|
],
|
|
content,
|
|
onUpdate: ({ editor }) => {
|
|
onChange(editor.getHTML());
|
|
},
|
|
editorProps: {
|
|
attributes: {
|
|
class: 'prose prose-invert prose-sm max-w-none focus:outline-none min-h-[120px] px-3 py-2',
|
|
placeholder: placeholder || '',
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!editor) {
|
|
return null;
|
|
}
|
|
|
|
const ToolbarButton = ({
|
|
onClick,
|
|
isActive = false,
|
|
children,
|
|
title,
|
|
}: {
|
|
onClick: () => void;
|
|
isActive?: boolean;
|
|
children: React.ReactNode;
|
|
title: string;
|
|
}) => (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
title={title}
|
|
className={`p-1.5 rounded hover:bg-bambu-dark-tertiary transition-colors ${
|
|
isActive ? 'bg-bambu-dark-tertiary text-bambu-green' : 'text-bambu-gray'
|
|
}`}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
|
|
const setLink = () => {
|
|
const url = window.prompt('Enter URL:');
|
|
if (url) {
|
|
editor.chain().focus().setLink({ href: url }).run();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="border border-bambu-dark-tertiary rounded-lg overflow-hidden bg-bambu-dark">
|
|
{/* Toolbar */}
|
|
<div className="flex items-center gap-0.5 p-1.5 border-b border-bambu-dark-tertiary bg-bambu-dark-secondary">
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
isActive={editor.isActive('bold')}
|
|
title={t('richTextEditor.bold')}
|
|
>
|
|
<Bold className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
isActive={editor.isActive('italic')}
|
|
title={t('richTextEditor.italic')}
|
|
>
|
|
<Italic className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
isActive={editor.isActive('underline')}
|
|
title={t('richTextEditor.underline')}
|
|
>
|
|
<UnderlineIcon className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
|
|
<div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
|
|
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
isActive={editor.isActive('bulletList')}
|
|
title={t('richTextEditor.bulletList')}
|
|
>
|
|
<List className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
isActive={editor.isActive('orderedList')}
|
|
title={t('richTextEditor.numberedList')}
|
|
>
|
|
<ListOrdered className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
|
|
<div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
|
|
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
isActive={editor.isActive({ textAlign: 'left' })}
|
|
title={t('richTextEditor.alignLeft')}
|
|
>
|
|
<AlignLeft className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
|
isActive={editor.isActive({ textAlign: 'center' })}
|
|
title={t('richTextEditor.alignCenter')}
|
|
>
|
|
<AlignCenter className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
isActive={editor.isActive({ textAlign: 'right' })}
|
|
title={t('richTextEditor.alignRight')}
|
|
>
|
|
<AlignRight className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
|
|
<div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
|
|
|
|
<ToolbarButton
|
|
onClick={setLink}
|
|
isActive={editor.isActive('link')}
|
|
title={t('richTextEditor.addLink')}
|
|
>
|
|
<LinkIcon className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
{editor.isActive('link') && (
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().unsetLink().run()}
|
|
title={t('richTextEditor.removeLink')}
|
|
>
|
|
<Unlink className="w-4 h-4" />
|
|
</ToolbarButton>
|
|
)}
|
|
</div>
|
|
|
|
{/* Editor */}
|
|
<EditorContent editor={editor} />
|
|
</div>
|
|
);
|
|
}
|