vite_deleteelectron
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
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 = {}) => {
|
||||
if (!options.headers) options.headers = {};
|
||||
if (options.method && ['POST', 'PUT', 'DELETE'].includes(options.method.toUpperCase())) {
|
||||
options.headers['x-csrf-token'] = csrfToken;
|
||||
}
|
||||
return fetch(url, options);
|
||||
}, [csrfToken]);
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
if (editingBlockId) {
|
||||
await authenticatedFetch(`/moders/content/${activeTab}/${editingBlockId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'text', content })
|
||||
});
|
||||
showNotify('Блок обновлён', 'success');
|
||||
} else {
|
||||
await authenticatedFetch(`/moders/content/${activeTab}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'text', content })
|
||||
});
|
||||
showNotify('Блок добавлен', 'success');
|
||||
}
|
||||
setShowEditModal(false);
|
||||
loadBlocks(activeTab);
|
||||
} 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) {
|
||||
// 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;
|
||||
Reference in New Issue
Block a user