feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md). - 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой - Удалены legacy-views и переходный слой LegacyTabView (45 файлов) - Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки - CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf) - Фиксы: lazy-импорт именованных экспортов (краш после авторизации), растяжение #admin-root/#settings-root на всю ширину, возврат subjects/lessonTypes из справочников (краш просмотра расписаний), отказ от заведомо широкого поиска расписания без выбранной цели, центрирование карточки логина - Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину - Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
This commit is contained in:
186
frontend/react/shared/ui/ConfirmDialog.jsx
Normal file
186
frontend/react/shared/ui/ConfirmDialog.jsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function ConfirmDialog({
|
||||
title = 'Подтвердите действие',
|
||||
message = '',
|
||||
confirmLabel = 'Подтвердить',
|
||||
cancelLabel = 'Отмена',
|
||||
danger = false,
|
||||
onConfirm,
|
||||
onClose
|
||||
}) {
|
||||
const confirmRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
confirmRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeydown = event => {
|
||||
if (event.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="project-dialog-backdrop"
|
||||
onClick={event => {
|
||||
if (event.target === event.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<section
|
||||
className="project-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="project-dialog-title"
|
||||
aria-describedby="project-dialog-message"
|
||||
>
|
||||
<header>
|
||||
<h2 id="project-dialog-title">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon-md btn-ghost"
|
||||
aria-label="Закрыть"
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<p id="project-dialog-message">{message}</p>
|
||||
<footer>
|
||||
{cancelLabel && (
|
||||
<button type="button" className="btn btn-md btn-secondary" onClick={onClose}>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
ref={confirmRef}
|
||||
type="button"
|
||||
className={`btn btn-md ${danger ? 'btn-danger' : 'btn-primary'}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertDialog({ title, message, confirmLabel = 'Понятно', onClose }) {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel={null}
|
||||
confirmLabel={confirmLabel}
|
||||
message={message}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
onConfirm={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Реплика legacy promptAction: диалог с обязательным текстовым полем.
|
||||
// onConfirm получает trimmed-значение; пустое обязательное поле показывает
|
||||
// ошибку внутри диалога, как в admin/js/dialog.js.
|
||||
export function PromptDialog({
|
||||
title = 'Подтвердите действие',
|
||||
message = '',
|
||||
confirmLabel = 'Подтвердить',
|
||||
cancelLabel = 'Отмена',
|
||||
danger = false,
|
||||
input = {},
|
||||
onConfirm,
|
||||
onClose
|
||||
}) {
|
||||
const [value, setValue] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeydown = event => {
|
||||
if (event.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
function confirm() {
|
||||
const trimmed = value.trim();
|
||||
if (input.required && !trimmed) {
|
||||
setError(input.requiredMessage || 'Заполните поле');
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
onConfirm(trimmed);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="project-dialog-backdrop"
|
||||
onClick={event => {
|
||||
if (event.target === event.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<section
|
||||
className="project-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="project-dialog-title"
|
||||
aria-describedby="project-dialog-message"
|
||||
>
|
||||
<header>
|
||||
<h2 id="project-dialog-title">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon-md btn-ghost"
|
||||
aria-label="Закрыть"
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<p id="project-dialog-message">{message}</p>
|
||||
<label className="project-dialog-field">
|
||||
<span>{input.label || 'Комментарий'}</span>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
rows="4"
|
||||
maxLength={Number(input.maxlength || 2000)}
|
||||
placeholder={input.placeholder || ''}
|
||||
value={value}
|
||||
onChange={event => {
|
||||
setValue(event.target.value);
|
||||
setError('');
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="project-dialog-error" role="alert">{error}</div>
|
||||
<footer>
|
||||
{cancelLabel && (
|
||||
<button type="button" className="btn btn-md btn-secondary" onClick={onClose}>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-md ${danger ? 'btn-danger' : 'btn-primary'}`}
|
||||
onClick={confirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user