313 lines
14 KiB
React
313 lines
14 KiB
React
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('<p>Новый блок</p>');
|
|
setEditPreviewContent(DOMPurify.sanitize('<p>Новый блок</p>'));
|
|
originalContentRef.current = '<p>Новый блок</p>';
|
|
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 === '<p><br></p>') 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 (
|
|
<>
|
|
<div id="notify" className={notify.show ? `show ${notify.type}` : ''}>{notify.message}</div>
|
|
<div className="container">
|
|
<h1>Панель модератора</h1>
|
|
{!isLoggedIn ? (
|
|
<div id="login-section">
|
|
<h2>Вход для модератора</h2>
|
|
<form id="moder-login-form" onSubmit={handleLogin}>
|
|
<div className="form-group">
|
|
<label htmlFor="username">Логин:</label>
|
|
<input type="text" id="username" name="username" required value={username} onChange={(e) => setUsername(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="password">Пароль:</label>
|
|
<input type="password" id="password" name="password" required value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
</div>
|
|
<button type="submit">Войти</button>
|
|
{loginError && <p id="login-error" style={{ color: 'red' }}>{loginError}</p>}
|
|
</form>
|
|
</div>
|
|
) : (
|
|
<div id="editor-section">
|
|
<button id="logout-btn" style={{ float: 'right' }} onClick={handleLogout}>Выйти из системы</button>
|
|
<h2>Редактирование вкладок</h2>
|
|
<div className="tab-controls">
|
|
<button className={`tab-btn ${activeTab === 'info' ? 'active' : ''}`} onClick={() => handleTabChange('info')} data-tab="info">Общая информация</button>
|
|
<button className={`tab-btn ${activeTab === 'union' ? 'active' : ''}`} onClick={() => handleTabChange('union')} data-tab="union">Профсоюз</button>
|
|
</div>
|
|
<div style={{ margin: '18px 0' }}>
|
|
<button id="add-block-btn" onClick={handleAddBlock}>Добавить блок</button>
|
|
</div>
|
|
<div id="blocks-list">
|
|
{blocks.length === 0 && <p>Нет блоков</p>}
|
|
{blocks.map(block => (
|
|
<div key={block.id} className="content-block">
|
|
<div className="block-preview" style={{ marginBottom: '10px' }} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(block.content) }} />
|
|
<button onClick={() => handleEditBlock(block)}>Редактировать</button>
|
|
<button onClick={() => handleDeleteBlock(block.id)}>Удалить</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showEditModal && (
|
|
<div id="edit-modal" className="modal" style={{ display: 'block' }} onClick={(e) => {
|
|
if (e.target === e.currentTarget) handleTryCloseEditModal();
|
|
}}>
|
|
<div className="modal-content">
|
|
<span className="close" onClick={handleTryCloseEditModal}>×</span>
|
|
<h3 id="edit-modal-title">{editingBlockId ? 'Редактировать блок' : 'Добавить блок'}</h3>
|
|
<form id="edit-block-form" onSubmit={handleSaveBlock}>
|
|
<input type="hidden" id="edit-block-id" value={editingBlockId || ''} />
|
|
<div className="form-group">
|
|
<label htmlFor="edit-quill-editor">Холст:</label>
|
|
<div id="edit-quill-editor" ref={editorRef} style={{ height: '250px' }}></div>
|
|
</div>
|
|
<div id="edit-preview-label" style={{ marginTop: '10px', fontWeight: 'bold' }}>Предпросмотр:</div>
|
|
<div id="edit-block-preview" className="block-text" style={{ minHeight: '40px', marginBottom: '10px' }} dangerouslySetInnerHTML={{ __html: editPreviewContent }}></div>
|
|
<button type="submit">Сохранить</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showConfirmModal && (
|
|
<div id="confirm-modal" className="modal" style={{ display: 'block', zIndex: 2000 }}>
|
|
<div className="modal-content" style={{ maxWidth: '400px', textAlign: 'center' }}>
|
|
<p style={{ marginBottom: '20px' }}>Вы уверены, что хотите закрыть окно?<br />Несохранённые изменения будут потеряны!</p>
|
|
<button id="confirm-close-yes" style={{ background: '#e74c3c', marginRight: '12px' }} onClick={handleConfirmClose}>Да, закрыть</button>
|
|
<button id="confirm-close-no" onClick={handleCancelClose}>Отмена</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default Editor; |