import React, { useState, useEffect, useRef, useCallback } from 'react'; import 'quill/dist/quill.snow.css'; import '../App.css'; import DOMPurify from 'dompurify'; function Editor() { const [isLoggedIn, setIsLoggedIn] = useState(false); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loginError, setLoginError] = useState(''); const [activeTab, setActiveTab] = useState('info'); const [blocks, setBlocks] = useState([]); const [showEditModal, setShowEditModal] = useState(false); const [editingBlockId, setEditingBlockId] = useState(null); const [editContent, setEditContent] = useState(''); const [editPreviewContent, setEditPreviewContent] = useState(''); const [showConfirmModal, setShowConfirmModal] = useState(false); //const [csrfToken, setCsrfToken] = useState(''); const [notify, setNotify] = useState({ message: '', type: '', show: false }); const editorRef = useRef(null); const quillRef = useRef(null); const originalContentRef = useRef(''); // Notify helper const showNotify = useCallback((message, type = 'success') => { setNotify({ message, type, show: true }); setTimeout(() => { setNotify({ message: '', type: '', show: false }); }, 2200); }, []); // Fetch CSRF token //useEffect(() => { // fetch('/moders/csrf-token') // .then(res => res.json()) // .then(data => { // setCsrfToken(data.csrfToken); //}) //.catch(() => {}); //}, []); // Check session on mount useEffect(() => { fetch('/moders/check-session', { credentials: 'include' }) .then(res => { if (res.ok) { setIsLoggedIn(true); loadBlocks(activeTab); } else { setIsLoggedIn(false); } }) .catch(() => setIsLoggedIn(false)); }, []); // eslint-disable-line react-hooks/exhaustive-deps const authenticatedFetch = useCallback((url, options = {}) => { return fetch(url, options); }, []); const loadBlocks = useCallback(async (tab) => { try { const res = await fetch(`/moders/content/${tab}`); const data = await res.json(); setBlocks(Array.isArray(data) ? data : []); } catch (e) { setBlocks([]); } }, []); const handleLogin = async (e) => { e.preventDefault(); setLoginError(''); try { const res = await fetch('/moders/moder-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (res.ok) { setIsLoggedIn(true); setLoginError(''); loadBlocks(activeTab); } else { const data = await res.json(); setLoginError(data.message || 'Ошибка входа'); } } catch (err) { setLoginError('Ошибка сервера'); } }; const handleLogout = async () => { try { await authenticatedFetch('/moders/moder-logout'); } catch (e) { // ignore } setIsLoggedIn(false); setUsername(''); setPassword(''); showNotify('Вы вышли из системы', 'success'); }; const handleTabChange = (tab) => { setActiveTab(tab); if (isLoggedIn) { loadBlocks(tab); } }; const handleAddBlock = () => { setEditingBlockId(null); setEditContent('

Новый блок

'); setEditPreviewContent(DOMPurify.sanitize('

Новый блок

')); originalContentRef.current = '

Новый блок

'; setShowEditModal(true); }; const handleEditBlock = (block) => { setEditingBlockId(block.id); setEditContent(block.content); setEditPreviewContent(DOMPurify.sanitize(block.content)); originalContentRef.current = block.content; setShowEditModal(true); }; const handleDeleteBlock = async (blockId) => { if (!window.confirm('Удалить блок?')) return; try { await authenticatedFetch(`/moders/content/${activeTab}/${blockId}`, { method: 'DELETE' }); showNotify('Блок удалён', 'success'); loadBlocks(activeTab); } catch (e) { showNotify('Ошибка удаления', 'error'); } }; const handleSaveBlock = async (e) => { e.preventDefault(); const content = DOMPurify.sanitize(editContent.trim()); if (!content || content === '


') return; const url = editingBlockId ? `/moders/content/${activeTab}/${editingBlockId}` : `/moders/content/${activeTab}`; const method = editingBlockId ? 'PUT' : 'POST'; console.log('➡️ Отправка:', method, url, { type: 'text', content }); try { const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'text', content }) }); const data = await response.json(); console.log('⬅️ Ответ сервера:', response.status, data); if (response.ok) { showNotify(editingBlockId ? 'Блок обновлён' : 'Блок добавлен', 'success'); setShowEditModal(false); loadBlocks(activeTab); } else { showNotify(`Ошибка: ${data.message || response.status}`, 'error'); } } catch (e) { console.error('❌ Ошибка при сохранении:', e); showNotify('Ошибка сохранения', 'error'); } }; const handleTryCloseEditModal = () => { if (editContent !== originalContentRef.current) { setShowConfirmModal(true); } else { setShowEditModal(false); } }; const handleConfirmClose = () => { setShowConfirmModal(false); setShowEditModal(false); }; const handleCancelClose = () => { setShowConfirmModal(false); }; // Initialize Quill when modal opens useEffect(() => { if (showEditModal && editorRef.current && !quillRef.current) { // Dynamically import Quill import('quill').then((QuillModule) => { const Quill = QuillModule.default || QuillModule; if (editorRef.current) { quillRef.current = new Quill(editorRef.current, { theme: 'snow', modules: { toolbar: [ [{ header: [1, 2, 3, false] }], ['bold', 'italic', 'underline', 'strike'], [{ 'align': [] }], [{ 'color': [] }, { 'background': [] }], [{ 'list': 'ordered'}, { 'list': 'bullet' }], ['blockquote', 'code-block'], ['link', 'image', 'video'], ['clean'] ] } }); quillRef.current.clipboard.dangerouslyPasteHTML(editContent); quillRef.current.on('text-change', () => { const html = quillRef.current.root.innerHTML; setEditContent(html); setEditPreviewContent(DOMPurify.sanitize(html)); }); } }); } // Cleanup Quill when modal closes if (!showEditModal && quillRef.current) { quillRef.current = null; } }, [showEditModal, editContent]); return ( <>
{notify.message}

Панель модератора

{!isLoggedIn ? (

Вход для модератора

setUsername(e.target.value)} />
setPassword(e.target.value)} />
{loginError &&

{loginError}

}
) : (

Редактирование вкладок

{blocks.length === 0 &&

Нет блоков

} {blocks.map(block => (
))}
)} {showEditModal && (
{ if (e.target === e.currentTarget) handleTryCloseEditModal(); }}>
×

{editingBlockId ? 'Редактировать блок' : 'Добавить блок'}

Предпросмотр:
)} {showConfirmModal && (

Вы уверены, что хотите закрыть окно?
Несохранённые изменения будут потеряны!

)}
); } export default Editor;