feat(phase-6): event browsing UI — pre-match/live lists, detail page, +26 bUnit tests

Replaces PreMatch/Live placeholder pages with a shared EventListShell
(filter chips, date range, sortable virtualized-friendly table, debounced
search, live auto-refresh with odds-movement indicators) and adds a new
/events/{eventCode} detail page (asymmetric header lockup, dynamic
Match/Period tabs, Plotly.Blazor odds-over-time chart with accessible
data-table fallback, snapshot history, Excel export modal).

New primitives matching Phase 5's editorial-quant system:
- SportIcon: inline SVGs per sport (basketball=6, football=11,
  tennis=22723, hockey=43658, generic fallback)
- OddsCell: tabular mono with ▲/▼/— delta + flash on change
  (prefers-reduced-motion honored)
- OddsTimeline: Plotly.Blazor wrapper with theme-aware colors and
  <details>/<summary> data-table screen-reader fallback
- ExportDialog: From/To pickers + ExportKind radio + Esc/Enter
  keyboard, surfaces use-case errors inline
- EventListShell: shared section shell for PreMatch/Live cadence

State + service split keeps the RCL host-agnostic:
- IEventBrowsingService / EventBrowsingService — wraps repos, returns
  view-model records (EventListItem, EventDetail, EventScopeBoard,
  BetRow, OddsTimelinePoint, SnapshotHistoryEntry); pages never see
  EF or domain entities directly.
- EventBrowsingState — singleton (per-circuit in BlazorWebView) holding
  immutable PageFilter records for PreMatch and Live.

Plotly.Blazor 5.4.1 added (latest .NET 8 line; 7.x has breaking changes).
+59 RU/EN localization keys following the Phase 5 dot-segmented convention.

Tests: +26 bUnit tests (PreMatch/Live/Detail pages, OddsCell/SportIcon/
ExportDialog components, EventBrowsingState). Total 228/228 passing
(Domain 96 + Application 15 + Infrastructure 80 + UI 37; baseline 202).
Build clean (0/0).

PLAN.md: P2/P3/P5 top-level checkboxes ticked; P6 row marked Done.
This commit is contained in:
2026-05-05 12:58:03 +03:00
parent fe97643a41
commit 553db2bce3
32 changed files with 3060 additions and 64 deletions
@@ -162,4 +162,80 @@
<data name="Anomaly.Live"><value>Аномалия</value></data>
<data name="Anomaly.Kind.SuspensionFlip"><value>Разворот после заморозки</value></data>
<data name="Anomaly.Score"><value>Уверенность</value></data>
<!-- Phase 6 — Список матчей / Лайв / Детали / Экспорт -->
<data name="PreMatch.Title"><value>Расписание до матча</value></data>
<data name="PreMatch.Lede"><value>Предстоящие события с последним предматчевым превью «1 / X / 2». Фильтр по виду спорта, стране, лиге и команде.</value></data>
<data name="PreMatch.Empty"><value>Под текущие фильтры не подпадает ни одно событие. Расширьте диапазон или снимите чипы выше.</value></data>
<data name="PreMatch.Filter.Toolbar"><value>Панель фильтров</value></data>
<data name="PreMatch.Filter.From"><value>С</value></data>
<data name="PreMatch.Filter.To"><value>По</value></data>
<data name="PreMatch.Filter.Sport"><value>Виды спорта</value></data>
<data name="PreMatch.Filter.Country"><value>Страны</value></data>
<data name="PreMatch.Filter.Search"><value>Поиск по лиге или команде</value></data>
<data name="PreMatch.Filter.Search.Placeholder"><value>напр. Реал Мадрид, NBA, Ролан Гаррос…</value></data>
<data name="PreMatch.Column.Time"><value>Время</value></data>
<data name="PreMatch.Column.Country"><value>Страна</value></data>
<data name="PreMatch.Column.League"><value>Лига</value></data>
<data name="PreMatch.Column.Match"><value>Матч</value></data>
<data name="PreMatch.Footer.Events"><value>событий</value></data>
<data name="PreMatch.Footer.Refreshed"><value>обновлено в</value></data>
<data name="Live.Title"><value>Лайв-поток коэффициентов</value></data>
<data name="Live.Lede"><value>Текущие лайв-события с последним сделанным снимком. Список обновляется по настроенному интервалу опроса; строки пульсируют при движении котировки.</value></data>
<data name="Live.AutoRefresh"><value>Автообновление</value></data>
<data name="Detail.Title"><value>Событие</value></data>
<data name="Detail.NotFound"><value>Событие не найдено — возможно, оно убрано из исходного потока.</value></data>
<data name="Detail.BackToList"><value>К расписанию</value></data>
<data name="Detail.Export"><value>Экспорт</value></data>
<data name="Detail.Tabs.Aria"><value>Вкладки разделов ставок</value></data>
<data name="Detail.Tabs.Match"><value>Матч</value></data>
<data name="Detail.Tabs.Period"><value>Период {0}</value></data>
<data name="Detail.NoBoards"><value>Снимков ставок по этому событию ещё нет.</value></data>
<data name="Detail.BetType"><value>Тип</value></data>
<data name="Detail.Side"><value>Сторона</value></data>
<data name="Detail.Threshold"><value>Порог</value></data>
<data name="Detail.Rate"><value>Кэф</value></data>
<data name="Detail.BetType.Win"><value>Победа</value></data>
<data name="Detail.BetType.Draw"><value>Ничья</value></data>
<data name="Detail.BetType.WinFora"><value>Фора</value></data>
<data name="Detail.BetType.Total"><value>Тотал</value></data>
<data name="Detail.Side.Side1"><value>1</value></data>
<data name="Detail.Side.Side2"><value>2</value></data>
<data name="Detail.Side.Draw"><value>X</value></data>
<data name="Detail.Side.Less"><value>Меньше</value></data>
<data name="Detail.Side.More"><value>Больше</value></data>
<data name="Detail.Chart.Title"><value>Динамика коэффициентов</value></data>
<data name="Detail.Chart.Empty"><value>Снимков по этому событию ещё нет.</value></data>
<data name="Detail.Chart.Time"><value>Время</value></data>
<data name="Detail.Chart.Win1"><value>П1</value></data>
<data name="Detail.Chart.Draw"><value>X</value></data>
<data name="Detail.Chart.Win2"><value>П2</value></data>
<data name="Detail.Chart.AccessibleSummary"><value>Показать таблицу значений</value></data>
<data name="Detail.History.Title"><value>История снимков</value></data>
<data name="Detail.History.Source"><value>Источник</value></data>
<data name="Detail.History.BetCount"><value>Ставок</value></data>
<data name="Detail.History.Live"><value>ЛАЙВ</value></data>
<data name="Detail.History.PreMatch"><value>ДО МАТЧА</value></data>
<data name="Export.Kicker"><value>Экспорт</value></data>
<data name="Export.Title"><value>Экспорт в Excel</value></data>
<data name="Export.DateRange.From"><value>Дата начала</value></data>
<data name="Export.DateRange.To"><value>Дата конца</value></data>
<data name="Export.Kind.Label"><value>Тип снимков</value></data>
<data name="Export.Kind.PreMatch"><value>Только до матча</value></data>
<data name="Export.Kind.Live"><value>Только лайв</value></data>
<data name="Export.Kind.Combined"><value>Комбинированный</value></data>
<data name="Export.Submit"><value>Экспорт</value></data>
<data name="Export.Cancel"><value>Отмена</value></data>
<data name="Export.Success"><value>Файл сохранён в {0}</value></data>
<data name="Export.Error.MissingDates"><value>Выберите даты начала и конца.</value></data>
<data name="Export.Error.InvalidRange"><value>Дата конца должна быть не раньше даты начала.</value></data>
<data name="Export.Error.Failed"><value>Экспорт не удался.</value></data>
<data name="Sport.Basketball"><value>Баскетбол</value></data>
<data name="Sport.Football"><value>Футбол</value></data>
<data name="Sport.Tennis"><value>Теннис</value></data>
<data name="Sport.Hockey"><value>Хоккей</value></data>
</root>