Files
magistr/frontend/react/shared/ui/ConfirmDialog.jsx
dipatrik10 97bf3cc50a 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
2026-09-28 23:15:04 +03:00

187 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}