Files
electron/admin-frontend/src/components/Editor.jsx
T
2026-10-29 16:51:26 +03:00

330 lines
15 KiB
React

import React, { useState, useEffect, useRef, useCallback } from 'react';
import 'quill/dist/quill.snow.css';
import '../App.css';
import DOMPurify from 'dompurify';
import SlidesManager from './SlidesManager';
import FoodManager from './FoodManager';
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 [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);
}, []);
// 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 && tab !== 'slides' && tab !== 'food') {
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';
try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'text', content })
});
if (response.ok) {
showNotify(editingBlockId ? 'Блок обновлён' : 'Блок добавлен', 'success');
setShowEditModal(false);
loadBlocks(activeTab);
} else {
const data = await response.json();
showNotify(`Ошибка: ${data.message || response.status}`, 'error');
}
} catch (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) {
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));
});
}
});
}
if (!showEditModal && quillRef.current) {
quillRef.current = null;
}
}, [showEditModal, editContent]);
// Если не авторизован — показываем форму входа
if (!isLoggedIn) {
return (
<div className="container">
<h1>Панель модератора</h1>
<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>
);
}
// Авторизован — показываем редактор
return (
<>
<div id="notify" className={notify.show ? `show ${notify.type}` : ''}>{notify.message}</div>
<div className="container">
<h1>Панель модератора</h1>
<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 === 'events' ? 'active' : ''}`} onClick={() => handleTabChange('events')} data-tab="events">
📅 Мероприятия
</button>
<button className={`tab-btn ${activeTab === 'help' ? 'active' : ''}`} onClick={() => handleTabChange('help')} data-tab="help">
💰 Материальная помощь
</button>
<button className={`tab-btn ${activeTab === 'housing' ? 'active' : ''}`} onClick={() => handleTabChange('housing')} data-tab="housing">
🏠 Жилищные условия
</button>
{/* ✅ НОВАЯ ВКЛАДКА СЛАЙДЫ */}
<button className={`tab-btn ${activeTab === 'slides' ? 'active' : ''}`} onClick={() => setActiveTab('slides')} data-tab="slides">
📸 Слайды
</button>
<button className={`tab-btn ${activeTab === 'food' ? 'active' : ''}`} onClick={() => setActiveTab('food')} data-tab="food">
🍔 Питание
</button>
</div>
{/* Рендеринг контента вкладок */}
{activeTab === 'slides' ? (
<SlidesManager />
) : activeTab === 'food' ? (
<FoodManager />
) : (
<>
<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>
</>
)}
{/* Модалка редактирования блоков */}
{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}>&times;</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;