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:
dipatrik10
2026-09-28 23:15:04 +03:00
parent c6083b35e0
commit 97bf3cc50a
167 changed files with 31283 additions and 19033 deletions

View File

@@ -0,0 +1,93 @@
import {
buildScheduleOverviewModel,
formatLongDate,
lessonMeta,
lessonStart,
pluralLessons,
relativeDate,
trimTime
} from '../../../schedule-overview.js';
export function ScheduleOverview({ lessons, now = new Date(), onGoToday, role = 'student' }) {
const { todayIso, todayLessons, nextLesson } = buildScheduleOverviewModel(lessons, now);
return (
<section className="schedule-overview" aria-live="polite">
<header className="schedule-overview-head">
<div>
<span className="schedule-overview-kicker">Быстрый обзор</span>
<h2>{formatLongDate(now)}</h2>
</div>
<button className="btn btn-md btn-secondary" type="button" onClick={onGoToday}>
Открыть текущую неделю
</button>
</header>
<div className="schedule-overview-grid">
<article className="schedule-overview-card is-today">
<div className="schedule-overview-card-head">
<div>
<span>Сегодня</span>
<h3>{todayLessons.length ? pluralLessons(todayLessons.length) : 'Свободный день'}</h3>
</div>
<span className="schedule-overview-count">{todayLessons.length}</span>
</div>
<div className="schedule-overview-list">
{todayLessons.length > 0
? todayLessons.map((lesson, index) => (
<CompactLesson
key={`${lesson.date}-${trimTime(lesson.startTime)}-${index}`}
lesson={lesson}
role={role}
/>
))
: <p className="schedule-overview-empty">На сегодня занятий нет.</p>}
</div>
</article>
<article className="schedule-overview-card is-next">
<div className="schedule-overview-card-head">
<div>
<span>Следующая пара</span>
<h3>{nextLesson ? relativeDate(nextLesson.date, todayIso) : 'Пока ничего не запланировано'}</h3>
</div>
{nextLesson && (
<span className="schedule-overview-badge">
{lessonStart(nextLesson) <= now ? 'Идёт сейчас' : trimTime(nextLesson.startTime)}
</span>
)}
</div>
{nextLesson
? <NextLessonCard lesson={nextLesson} role={role} />
: <p className="schedule-overview-empty">В ближайшие 14 дней занятий нет.</p>}
</article>
</div>
</section>
);
}
function CompactLesson({ lesson, role }) {
return (
<div className="schedule-overview-lesson">
<time>{trimTime(lesson.startTime)}</time>
<div>
<strong>{lesson.subjectName || 'Занятие'}</strong>
<small>{lessonMeta(lesson, role).join(' · ') || 'Подробности уточняются'}</small>
</div>
</div>
);
}
function NextLessonCard({ lesson, role }) {
return (
<div className="schedule-overview-next">
<div className="schedule-overview-next-time">
<strong>{trimTime(lesson.startTime)}</strong>
<span>до {trimTime(lesson.endTime)}</span>
</div>
<div>
<h4>{lesson.subjectName || 'Занятие'}</h4>
<p>{lessonMeta(lesson, role).join(' · ') || 'Подробности уточняются'}</p>
{lesson.lessonTypeName && <span className="schedule-overview-type">{lesson.lessonTypeName}</span>}
</div>
</div>
);
}