import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import ImageExtension from '@tiptap/extension-image'; import LinkExtension from '@tiptap/extension-link'; import PlaceholderExtension from '@tiptap/extension-placeholder'; import TextAlignExtension from '@tiptap/extension-text-align'; import UnderlineExtension from '@tiptap/extension-underline'; import HighlightExtension from '@tiptap/extension-highlight'; import { VideoExtension } from '../lib/tiptap-video'; import { infoPagesApi } from '../api/infoPages'; import { newsApi } from '../api/news'; import { usePrompt } from '../store/promptDialog'; import { AdminBackButton } from '../components/admin'; import { Toggle } from '../components/admin/Toggle'; import { useHapticFeedback } from '../platform/hooks/useHaptic'; import { cn } from '../lib/utils'; import type { InfoPageType, FaqItem, ReplacesTab } from '../api/infoPages'; const AVAILABLE_LOCALES = ['ru', 'en', 'zh', 'fa'] as const; type LocaleCode = (typeof AVAILABLE_LOCALES)[number]; // --- Icons --- const BoldIcon = () => ( ); const ItalicIcon = () => ( ); const UnderlineIcon = () => ( ); const StrikeIcon = () => ( ); const H1Icon = () => H1; const H2Icon = () => H2; const H3Icon = () => H3; const ListBulletIcon = () => ( ); const ListOrderedIcon = () => ( ); const QuoteIcon = () => ( ); const CodeBlockIcon = () => ( ); const ImageIcon = () => ( ); const LinkIcon = () => ( ); const AlignLeftIcon = () => ( ); const AlignCenterIcon = () => ( ); const HighlightIcon = () => ( ); // --- Toolbar Button --- interface ToolbarButtonProps { onClick: () => void; isActive?: boolean; disabled?: boolean; title: string; children: React.ReactNode; } function ToolbarButton({ onClick, isActive, disabled, title, children }: ToolbarButtonProps) { return ( ); } // --- Security: URL scheme validation --- function isSafeUrl(url: string | null | undefined): boolean { if (!url) return false; try { const parsed = new URL(url); return parsed.protocol === 'https:' || parsed.protocol === 'http:'; } catch { return false; } } // --- Slug utility --- const TRANSLIT_MAP: Record = { а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'yo', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'kh', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'shch', ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya', }; function generateSlug(title: string): string { const lower = title.toLowerCase(); const transliterated = Array.from(lower) .map((ch) => TRANSLIT_MAP[ch] ?? ch) .join(''); return transliterated .replace(/[^a-z0-9\s-]/g, '') .replace(/[\s_]+/g, '-') .replace(/-+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 100); } // --- FAQ Q&A Item Icons --- const ChevronUpIcon = () => ( ); const ChevronDownIcon = () => ( ); const TrashSmallIcon = () => ( ); const PlusSmallIcon = () => ( ); // --- FAQ Answer Rich Editor --- const FAQ_EDITOR_EXTENSIONS = [ StarterKit.configure({ heading: { levels: [2, 3] }, link: false, underline: false, }), UnderlineExtension, LinkExtension.configure({ openOnClick: false, HTMLAttributes: { class: 'link' } }), ImageExtension.configure({ HTMLAttributes: { class: 'rounded-xl max-w-full' } }), PlaceholderExtension.configure({ placeholder: '' }), TextAlignExtension.configure({ types: ['heading', 'paragraph'] }), HighlightExtension, VideoExtension, ]; function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html: string) => void }) { const { t } = useTranslation(); const haptic = useHapticFeedback(); const mediaRef = useRef(null); const [uploadCount, setUploadCount] = useState(0); const isUploading = uploadCount > 0; const [isDragging, setIsDragging] = useState(false); const activeUploadsRef = useRef(new Set()); const onChangeRef = useRef(onChange); onChangeRef.current = onChange; const editor = useEditor({ extensions: FAQ_EDITOR_EXTENSIONS, content: value, editorProps: { attributes: { class: 'prose prose-sm max-w-none min-h-[120px] p-3 focus:outline-none', }, }, onUpdate: ({ editor: e }) => { const html = e.getHTML(); const isEmpty = html === '

' || html === ''; onChangeRef.current(isEmpty ? '' : html); }, }); // Sync external value changes (e.g., reorder, locale switch) useEffect(() => { if (!editor || editor.isDestroyed) return; const currentHtml = editor.getHTML(); const normalizedCurrent = currentHtml === '

' ? '' : currentHtml; if (normalizedCurrent !== value) { editor.commands.setContent(value || '', { emitUpdate: false }); } }, [value, editor]); // Cleanup uploads on unmount useEffect(() => { const uploads = activeUploadsRef.current; return () => { for (const c of uploads) c.abort(); uploads.clear(); }; }, []); const handleMediaUpload = useCallback( async (file: File) => { if (!editor) return; const isImage = file.type.startsWith('image/'); const isVideo = file.type.startsWith('video/'); if (!isImage && !isVideo) return; const maxSize = isImage ? 10 * 1024 * 1024 : 50 * 1024 * 1024; if (file.size > maxSize) { haptic.error(); return; } const controller = new AbortController(); activeUploadsRef.current.add(controller); setUploadCount((c) => c + 1); try { const result = await newsApi.uploadMedia(file, controller.signal); if (controller.signal.aborted) return; if (!isSafeUrl(result.url)) return; if (result.media_type === 'image') { editor.chain().focus().setImage({ src: result.url, alt: file.name }).run(); } else { editor .chain() .focus() .insertContent({ type: 'video', attrs: { src: result.url, class: 'w-full rounded-xl max-h-96' }, }) .run(); } haptic.success(); } catch { if (!controller.signal.aborted) haptic.error(); } finally { activeUploadsRef.current.delete(controller); setUploadCount((c) => c - 1); } }, [editor, haptic], ); const handleMediaUploadRef = useRef(handleMediaUpload); useEffect(() => { handleMediaUploadRef.current = handleMediaUpload; }, [handleMediaUpload]); // Register paste/drop handlers after editor is created useEffect(() => { if (!editor) return; const handlePaste = (_view: unknown, event: ClipboardEvent) => { const items = event.clipboardData?.items; if (!items) return false; for (const item of Array.from(items)) { if (item.type.startsWith('image/') || item.type.startsWith('video/')) { const file = item.getAsFile(); if (file) { handleMediaUploadRef.current(file); return true; } } } return false; }; const handleDrop = (_view: unknown, event: DragEvent) => { const file = event.dataTransfer?.files[0]; if (file && (file.type.startsWith('image/') || file.type.startsWith('video/'))) { event.preventDefault(); handleMediaUploadRef.current(file); return true; } return false; }; editor.setOptions({ editorProps: { ...editor.options.editorProps, handlePaste, handleDrop } }); }, [editor]); const promptDialog = usePrompt(); const addLink = useCallback(async () => { if (!editor) return; const url = await promptDialog({ label: t('news.admin.toolbar.linkUrlPrompt'), inputType: 'url', }); if (!url) return; try { const parsed = new URL(url); if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return; } catch { return; } editor.chain().focus().setLink({ href: url }).run(); }, [editor, t, promptDialog]); if (!editor) return null; return (
{/* Upload overlay */} {isUploading && (
{t('news.admin.uploading')}
)} {isDragging && !isUploading && (
{t('news.admin.dropMedia')}
)} {/* Compact toolbar */}
editor.chain().focus().toggleBold().run()} isActive={editor.isActive('bold')} title={t('news.admin.toolbar.bold')} > editor.chain().focus().toggleItalic().run()} isActive={editor.isActive('italic')} title={t('news.admin.toolbar.italic')} > editor.chain().focus().toggleUnderline().run()} isActive={editor.isActive('underline')} title={t('news.admin.toolbar.underline')} > editor.chain().focus().toggleStrike().run()} isActive={editor.isActive('strike')} title={t('news.admin.toolbar.strikethrough')} >
editor.chain().focus().toggleHeading({ level: 2 }).run()} isActive={editor.isActive('heading', { level: 2 })} title={t('news.admin.toolbar.heading2')} > editor.chain().focus().toggleBulletList().run()} isActive={editor.isActive('bulletList')} title={t('news.admin.toolbar.bulletList')} > editor.chain().focus().toggleOrderedList().run()} isActive={editor.isActive('orderedList')} title={t('news.admin.toolbar.orderedList')} > editor.chain().focus().toggleBlockquote().run()} isActive={editor.isActive('blockquote')} title={t('news.admin.toolbar.blockquote')} >
editor.chain().focus().toggleHighlight().run()} isActive={editor.isActive('highlight')} title={t('news.admin.toolbar.highlight')} > mediaRef.current?.click()} disabled={isUploading} title={t('news.admin.toolbar.image')} > {isUploading ? (
) : ( )}
{/* Editor */}
{ e.preventDefault(); setIsDragging(true); }} onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} onDrop={(e) => { setIsDragging(false); if (e.defaultPrevented) return; const file = e.dataTransfer.files[0]; if (file && (file.type.startsWith('image/') || file.type.startsWith('video/'))) { e.preventDefault(); handleMediaUpload(file); } }} >
{/* Hidden file input */} { const file = e.target.files?.[0]; if (file) handleMediaUpload(file); e.target.value = ''; }} className="hidden" aria-hidden="true" />
); } // --- FAQ Q&A Builder --- interface FaqBuilderProps { items: FaqItem[]; onChange: (items: FaqItem[]) => void; locale: string; localeLabel: string; } function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) { const { t } = useTranslation(); // Stable keys for React — travel with items through reorder/delete const keyCounter = useRef(0); const [itemKeys, setItemKeys] = useState(() => items.map(() => keyCounter.current++)); // Regenerate all keys on locale switch (items are semantically different) useEffect(() => { setItemKeys(items.map(() => keyCounter.current++)); }, [locale]); // eslint-disable-line react-hooks/exhaustive-deps // Sync key count when items added/removed within the same locale useEffect(() => { setItemKeys((prev) => { if (prev.length === items.length) return prev; if (items.length > prev.length) { const newKeys = [...prev]; for (let i = prev.length; i < items.length; i++) { newKeys.push(keyCounter.current++); } return newKeys; } return prev.slice(0, items.length); }); }, [items.length]); const handleQuestionChange = useCallback( (index: number, value: string) => { const updated = items.map((item, i) => (i === index ? { ...item, q: value } : item)); onChange(updated); }, [items, onChange], ); const handleAnswerChange = useCallback( (index: number, value: string) => { const updated = items.map((item, i) => (i === index ? { ...item, a: value } : item)); onChange(updated); }, [items, onChange], ); const handleRemove = useCallback( (index: number) => { setItemKeys((prev) => prev.filter((_, i) => i !== index)); onChange(items.filter((_, i) => i !== index)); }, [items, onChange], ); const handleAdd = useCallback(() => { const newKey = keyCounter.current++; setItemKeys((prev) => [...prev, newKey]); onChange([...items, { q: '', a: '' }]); }, [items, onChange]); const handleMoveUp = useCallback( (index: number) => { if (index === 0) return; const updated = [...items]; [updated[index - 1], updated[index]] = [updated[index], updated[index - 1]]; setItemKeys((prev) => { const k = [...prev]; [k[index - 1], k[index]] = [k[index], k[index - 1]]; return k; }); onChange(updated); }, [items, onChange], ); const handleMoveDown = useCallback( (index: number) => { if (index >= items.length - 1) return; const updated = [...items]; [updated[index], updated[index + 1]] = [updated[index + 1], updated[index]]; setItemKeys((prev) => { const k = [...prev]; [k[index], k[index + 1]] = [k[index + 1], k[index]]; return k; }); onChange(updated); }, [items, onChange], ); return (
{items.length} {t('admin.infoPages.faq.questionsCount')}
{items.length === 0 && (
{t('admin.infoPages.faq.noQuestions')}
)} {items.map((item, index) => (
{t('admin.infoPages.faq.questionNumber', { n: index + 1 })}
handleQuestionChange(index, e.target.value)} className="input text-sm" placeholder={t('admin.infoPages.faq.questionPlaceholder')} />
handleAnswerChange(index, html)} />
))}
); } export default function AdminInfoPageEditor() { const { t } = useTranslation(); const navigate = useNavigate(); const { id: rawId } = useParams<{ id: string }>(); const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); const haptic = useHapticFeedback(); const pageId = rawId != null ? Number(rawId) : undefined; const isEdit = pageId != null && !Number.isNaN(pageId); const initialPageType = (searchParams.get('type') === 'faq' ? 'faq' : 'page') as InfoPageType; // Form state const [slug, setSlug] = useState(''); const [slugManuallyEdited, setSlugManuallyEdited] = useState(false); const [icon, setIcon] = useState(''); const [isActive, setIsActive] = useState(true); const [sortOrder, setSortOrder] = useState(0); const [pageType, setPageType] = useState(initialPageType); const [replacesTab, setReplacesTab] = useState(null); const [saveError, setSaveError] = useState(null); // FAQ Q&A state per locale const [faqItems, setFaqItems] = useState>({}); // Multi-locale state const [activeLocale, setActiveLocale] = useState('ru'); const [titles, setTitles] = useState>({}); const [contents, setContents] = useState>({}); // Media upload state const mediaInputRef = useRef(null); const [uploadCount, setUploadCount] = useState(0); const isUploading = uploadCount > 0; const [isDragging, setIsDragging] = useState(false); const activeUploadsRef = useRef(new Set()); const handleMediaUploadRef = useRef<(file: File) => void>(() => {}); // TipTap editor const extensions = useMemo( () => [ StarterKit.configure({ heading: { levels: [1, 2, 3] }, link: false, underline: false, }), UnderlineExtension, LinkExtension.configure({ openOnClick: false, HTMLAttributes: { class: 'link' }, }), ImageExtension.configure({ HTMLAttributes: { class: 'rounded-xl max-w-full' }, }), PlaceholderExtension.configure({ placeholder: t('admin.infoPages.fields.content'), }), TextAlignExtension.configure({ types: ['heading', 'paragraph'], }), HighlightExtension, VideoExtension, ], // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const editor = useEditor({ extensions, editorProps: { attributes: { class: 'prose max-w-none min-h-[300px] p-4 focus:outline-none', }, handlePaste: (_view, event) => { const items = event.clipboardData?.items; if (!items) return false; for (const item of Array.from(items)) { if (item.type.startsWith('image/') || item.type.startsWith('video/')) { const file = item.getAsFile(); if (file) { handleMediaUploadRef.current(file); return true; } } } return false; }, handleDrop: (_view, event) => { const file = event.dataTransfer?.files[0]; if (file && (file.type.startsWith('image/') || file.type.startsWith('video/'))) { event.preventDefault(); handleMediaUploadRef.current(file); return true; } return false; }, }, }); // --- Media upload handlers --- const handleMediaUpload = useCallback( async (file: File) => { if (!editor) return; const isImage = file.type.startsWith('image/'); const isVideo = file.type.startsWith('video/'); if (!isImage && !isVideo) return; const maxSize = isImage ? 10 * 1024 * 1024 : 50 * 1024 * 1024; if (file.size > maxSize) { haptic.error(); setSaveError(t('news.admin.fileTooLarge')); return; } const controller = new AbortController(); activeUploadsRef.current.add(controller); setUploadCount((c) => c + 1); try { const result = await newsApi.uploadMedia(file, controller.signal); if (controller.signal.aborted) return; if (!isSafeUrl(result.url)) { haptic.error(); setSaveError(t('news.admin.uploadError')); return; } if (result.media_type === 'image') { editor.chain().focus().setImage({ src: result.url, alt: file.name }).run(); } else { editor .chain() .focus() .insertContent({ type: 'video', attrs: { src: result.url, class: 'w-full rounded-xl max-h-96' }, }) .run(); } haptic.success(); } catch { if (controller.signal.aborted) return; haptic.error(); setSaveError(t('news.admin.uploadError')); } finally { activeUploadsRef.current.delete(controller); setUploadCount((c) => c - 1); } }, [editor, haptic, t], ); useEffect(() => { handleMediaUploadRef.current = handleMediaUpload; }, [handleMediaUpload]); useEffect(() => { const uploads = activeUploadsRef.current; return () => { for (const controller of uploads) { controller.abort(); } uploads.clear(); }; }, []); const handleFileInputChange = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) handleMediaUpload(file); e.target.value = ''; }, [handleMediaUpload], ); const handleEditorDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleEditorDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleEditorDrop = useCallback( (e: React.DragEvent) => { setIsDragging(false); if (e.defaultPrevented) return; e.preventDefault(); const file = e.dataTransfer.files[0]; if (file) handleMediaUpload(file); }, [handleMediaUpload], ); // Fetch all admin pages to detect tab conflicts const { data: allAdminPages } = useQuery({ queryKey: ['admin', 'info-pages', 'list', 'all'], queryFn: () => infoPagesApi.getAdminPages(), staleTime: 30_000, }); // Fetch page for editing const { data: pageData, isLoading: isLoadingPage } = useQuery({ queryKey: ['admin', 'info-pages', 'page', pageId], queryFn: () => { if (pageId == null) throw new Error('Missing page id parameter'); return infoPagesApi.getAdminPage(pageId); }, enabled: isEdit, staleTime: 0, gcTime: 0, refetchOnMount: true, refetchOnWindowFocus: false, }); // Populate form when page data loads (once only -- not on locale switch) const editorPopulated = useRef(false); const formPopulated = useRef(false); useEffect(() => { if (!pageData || formPopulated.current) return; setSlug(pageData.slug); setSlugManuallyEdited(true); setIcon(pageData.icon ?? ''); setIsActive(pageData.is_active); setSortOrder(pageData.sort_order); setPageType(pageData.page_type ?? 'page'); setReplacesTab(pageData.replaces_tab ?? null); setTitles(pageData.title); if (pageData.page_type === 'faq') { // For FAQ pages, content stores Q&A arrays per locale const parsed: Record = {}; for (const [loc, val] of Object.entries(pageData.content)) { try { const arr = typeof val === 'string' ? JSON.parse(val) : val; parsed[loc] = Array.isArray(arr) ? arr : []; } catch { parsed[loc] = []; } } setFaqItems(parsed); setContents({}); } else { setContents(pageData.content); } formPopulated.current = true; }, [pageData]); // Set editor content once when editor is ready useEffect(() => { if (!pageData || !editor || editorPopulated.current) return; const initialContent = pageData.content[activeLocale] ?? pageData.content['ru'] ?? ''; editor.commands.setContent(initialContent); editorPopulated.current = true; }, [pageData, editor]); // activeLocale intentionally omitted // Auto-generate slug from Russian title useEffect(() => { if (!slugManuallyEdited && titles['ru']) { setSlug(generateSlug(titles['ru'])); } }, [titles, slugManuallyEdited]); // --- Locale switching --- const switchLocale = useCallback( (newLocale: LocaleCode) => { if (!editor) return; // Save current editor content const currentHtml = editor.getHTML(); const isEmpty = currentHtml === '

' || currentHtml === ''; setContents((prev) => ({ ...prev, [activeLocale]: isEmpty ? '' : currentHtml, })); // Load new locale content const newContent = contents[newLocale] ?? ''; editor.commands.setContent(newContent); setActiveLocale(newLocale); }, [editor, activeLocale, contents], ); // Save mutation const saveMutation = useMutation({ mutationFn: (data: { slug: string; title: Record; content: Record; page_type: InfoPageType; is_active: boolean; sort_order: number; icon: string | null; replaces_tab: ReplacesTab | null; }) => { if (isEdit && pageId != null) { return infoPagesApi.updatePage(pageId, data); } return infoPagesApi.createPage(data); }, onSuccess: () => { haptic.success(); queryClient.invalidateQueries({ queryKey: ['admin', 'info-pages'] }); queryClient.invalidateQueries({ queryKey: ['info-pages'] }); navigate('/admin/info-pages'); }, onError: (error: Error) => { haptic.error(); setSaveError(error.message || t('admin.infoPages.saveError')); }, }); const handleSave = () => { setSaveError(null); if (!slug.trim()) return; let finalContents: Record; if (pageType === 'faq') { // Serialize FAQ items as JSON strings per locale finalContents = {}; for (const [loc, items] of Object.entries(faqItems)) { finalContents[loc] = JSON.stringify(items); } } else { // Capture current editor content for the active locale const currentHtml = editor?.getHTML() ?? ''; const isEmpty = currentHtml === '

' || currentHtml === ''; finalContents = { ...contents, [activeLocale]: isEmpty ? '' : currentHtml, }; } const data = { slug: slug.trim(), title: titles, content: finalContents, page_type: pageType, is_active: isActive, sort_order: sortOrder, icon: icon.trim() || null, replaces_tab: replacesTab, }; haptic.buttonPress(); saveMutation.mutate(data); }; // Toolbar actions const promptDialog = usePrompt(); const addLink = async () => { if (!editor) return; const url = await promptDialog({ label: t('news.admin.toolbar.linkUrlPrompt'), inputType: 'url', }); if (!url) return; try { const parsed = new URL(url); if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return; } catch { return; } editor.chain().focus().setLink({ href: url }).run(); }; if (isEdit && isLoadingPage) { return (
); } return (
{/* Header */}

{isEdit ? t('admin.infoPages.edit') : t('admin.infoPages.create')}

{/* Form */}
{/* Slug */}
{ setSlug(e.target.value); setSlugManuallyEdited(true); }} className="input font-mono text-sm" required />
{/* Icon + Sort Order row */}
setIcon(e.target.value)} className="input" placeholder="📄" />
setSortOrder(Number(e.target.value) || 0)} min={0} className="input max-w-xs" />
{/* Active toggle */}
setIsActive((v) => !v)} aria-label={t('admin.infoPages.fields.isActive')} /> {t('admin.infoPages.fields.isActive')}
{/* Page type selector */}
{(['page', 'faq'] as const).map((pt) => ( ))}
{/* Replaces tab selector */}
{replacesTab && allAdminPages?.some((p) => p.replaces_tab === replacesTab && p.id !== pageId) && (

{t('admin.infoPages.replacesTabWarning')}

)}
{/* Locale tabs */}
{AVAILABLE_LOCALES.map((loc) => ( ))}
{/* Title for current locale */}
setTitles((prev) => ({ ...prev, [activeLocale]: e.target.value }))} className="input" />
{/* Content: TipTap editor for pages, FAQ builder for FAQ */} {pageType === 'faq' ? ( setFaqItems((prev) => ({ ...prev, [activeLocale]: items }))} locale={activeLocale} localeLabel={t(`admin.infoPages.locales.${activeLocale}`)} /> ) : (
{/* Upload progress overlay */} {isUploading && (
{t('news.admin.uploading')}
)} {/* Drag overlay */} {isDragging && !isUploading && (
{t('news.admin.dropMedia')}
)} {/* Toolbar */} {editor && (
editor.chain().focus().toggleBold().run()} isActive={editor.isActive('bold')} title={t('news.admin.toolbar.bold')} > editor.chain().focus().toggleItalic().run()} isActive={editor.isActive('italic')} title={t('news.admin.toolbar.italic')} > editor.chain().focus().toggleUnderline().run()} isActive={editor.isActive('underline')} title={t('news.admin.toolbar.underline')} > editor.chain().focus().toggleStrike().run()} isActive={editor.isActive('strike')} title={t('news.admin.toolbar.strikethrough')} >
editor.chain().focus().toggleHeading({ level: 1 }).run()} isActive={editor.isActive('heading', { level: 1 })} title={t('news.admin.toolbar.heading1')} > editor.chain().focus().toggleHeading({ level: 2 }).run()} isActive={editor.isActive('heading', { level: 2 })} title={t('news.admin.toolbar.heading2')} > editor.chain().focus().toggleHeading({ level: 3 }).run()} isActive={editor.isActive('heading', { level: 3 })} title={t('news.admin.toolbar.heading3')} >
editor.chain().focus().toggleBulletList().run()} isActive={editor.isActive('bulletList')} title={t('news.admin.toolbar.bulletList')} > editor.chain().focus().toggleOrderedList().run()} isActive={editor.isActive('orderedList')} title={t('news.admin.toolbar.orderedList')} > editor.chain().focus().toggleBlockquote().run()} isActive={editor.isActive('blockquote')} title={t('news.admin.toolbar.blockquote')} > editor.chain().focus().toggleCodeBlock().run()} isActive={editor.isActive('codeBlock')} title={t('news.admin.toolbar.codeBlock')} >
editor.chain().focus().setTextAlign('left').run()} isActive={editor.isActive({ textAlign: 'left' })} title={t('news.admin.toolbar.alignLeft')} > editor.chain().focus().setTextAlign('center').run()} isActive={editor.isActive({ textAlign: 'center' })} title={t('news.admin.toolbar.alignCenter')} >
editor.chain().focus().toggleHighlight().run()} isActive={editor.isActive('highlight')} title={t('news.admin.toolbar.highlight')} > mediaInputRef.current?.click()} disabled={isUploading} title={t('news.admin.toolbar.image')} > {isUploading ? (
) : ( )}
)} {/* Editor content */}
{/* Hidden file inputs */}
)} {/* Error feedback */} {saveError && (
{saveError}
)} {/* Bottom save button */}
); }