feat(phase-8-frontend): results loader UI + browsing list + 41 localization keys

* Pages/Results/ResultsList.razor — completed-events list with date range,
  sport/winner filter, search, footer count.
* Pages/Results/ResultsLoader.razor — driver page with two modes (load all
  in range / load selected events), live progress reporting via
  IProgress<PullResultsProgress>, summary line, cancellable.
* Replaces the Phase 5 Pages/Results.razor placeholder.

Service layer:
* IResultsBrowsingService + ResultsBrowsingService (Scoped, mirrors the
  Event/Anomaly browsing-service pattern). Reads IResultRepository +
  IEventRepository, projects to immutable view-model records.
* UiServicesExtensions: registers ResultsBrowsingService; also fixes an
  unrelated localization resolver bug (drop ResourcesPath since
  SharedResource lives in the Marathon.UI.Resources namespace already).

Localization:
* 41 new Results.* keys (RU+EN parity) covering both pages, filter chips,
  loader modes, progress states, and footer copy.

Tests:
* ResultsListTests + ResultsLoaderTests — 22 new bUnit tests covering
  filter narrowing, mode switching, progress aggregation, and empty
  states.
* FakeResultsBrowsingService support type for tests.
* MarathonTestContext registers the fake; TestData adds factories for
  EventResult/EventResultListItem.
This commit is contained in:
2026-05-09 15:10:49 +03:00
parent 9c5d3df1f2
commit 9f090cec1f
13 changed files with 1407 additions and 6 deletions
@@ -258,4 +258,47 @@
<data name="Sport.Football"><value>Football</value></data>
<data name="Sport.Tennis"><value>Tennis</value></data>
<data name="Sport.Hockey"><value>Hockey</value></data>
<data name="Results.Title"><value>Match results</value></data>
<data name="Results.Lede"><value>Final scores of loaded events. We walk each event page, wait for matchIsComplete=true, and record the winning side.</value></data>
<data name="Results.Action.LoadNew"><value>Load results</value></data>
<data name="Results.Action.OpenList"><value>Back to list</value></data>
<data name="Results.Filter.From"><value>From</value></data>
<data name="Results.Filter.To"><value>To</value></data>
<data name="Results.Filter.Search"><value>Search</value></data>
<data name="Results.Filter.Search.Placeholder"><value>Team, league, category…</value></data>
<data name="Results.Filter.Sport"><value>Sport</value></data>
<data name="Results.Filter.Winner"><value>Winner</value></data>
<data name="Results.Filter.Winner.All"><value>Any</value></data>
<data name="Results.Filter.Winner.Side1"><value>Side 1</value></data>
<data name="Results.Filter.Winner.Side2"><value>Side 2</value></data>
<data name="Results.Filter.Winner.Draw"><value>Draw</value></data>
<data name="Results.Column.Time"><value>Time</value></data>
<data name="Results.Column.Country"><value>Country</value></data>
<data name="Results.Column.League"><value>League</value></data>
<data name="Results.Column.Match"><value>Match</value></data>
<data name="Results.Column.Score"><value>Score</value></data>
<data name="Results.Column.Winner"><value>Winner</value></data>
<data name="Results.Column.CompletedAt"><value>Completed</value></data>
<data name="Results.Empty"><value>No results loaded for this range yet. Run a load or wait for matches to complete.</value></data>
<data name="Results.Footer.Items"><value>results</value></data>
<data name="Results.Loader.Kicker"><value>Loader</value></data>
<data name="Results.Loader.Title"><value>Load results</value></data>
<data name="Results.Loader.Lede"><value>We poll each event page, capture the final score, and record the winning side. Pick a date range or specific events.</value></data>
<data name="Results.Loader.Mode"><value>Mode</value></data>
<data name="Results.Loader.Mode.AllInRange"><value>All in range</value></data>
<data name="Results.Loader.Mode.Selected"><value>Selected events</value></data>
<data name="Results.Loader.Selected.Empty"><value>Every event in this range already has a result.</value></data>
<data name="Results.Loader.Selected.CountFormat"><value>{0} selected</value></data>
<data name="Results.Loader.Action.Load"><value>Load</value></data>
<data name="Results.Loader.Action.Cancel"><value>Cancel</value></data>
<data name="Results.Loader.Action.Back"><value>Back</value></data>
<data name="Results.Loader.Progress.Format"><value>{0} / {1}</value></data>
<data name="Results.Loader.Progress.Loaded"><value>Loaded</value></data>
<data name="Results.Loader.Progress.AlreadyLoaded"><value>Already loaded</value></data>
<data name="Results.Loader.Progress.NotYetComplete"><value>Not yet complete</value></data>
<data name="Results.Loader.Progress.Failed"><value>Failed</value></data>
<data name="Results.Loader.Summary.Format"><value>Loaded {0}, skipped {1}, processed {2} total.</value></data>
<data name="Results.Loader.Empty.NoCandidates"><value>No events to load in this range.</value></data>
</root>
@@ -271,4 +271,47 @@
<data name="Sport.Football"><value>Футбол</value></data>
<data name="Sport.Tennis"><value>Теннис</value></data>
<data name="Sport.Hockey"><value>Хоккей</value></data>
<data name="Results.Title"><value>Результаты матчей</value></data>
<data name="Results.Lede"><value>Финальные счета загруженных событий. Обходим страницу события, ждём matchIsComplete=true, фиксируем сторону-победителя.</value></data>
<data name="Results.Action.LoadNew"><value>Загрузить результаты</value></data>
<data name="Results.Action.OpenList"><value>К списку</value></data>
<data name="Results.Filter.From"><value>С</value></data>
<data name="Results.Filter.To"><value>По</value></data>
<data name="Results.Filter.Search"><value>Поиск</value></data>
<data name="Results.Filter.Search.Placeholder"><value>Команда, лига, категория…</value></data>
<data name="Results.Filter.Sport"><value>Спорт</value></data>
<data name="Results.Filter.Winner"><value>Победитель</value></data>
<data name="Results.Filter.Winner.All"><value>Любой</value></data>
<data name="Results.Filter.Winner.Side1"><value>Команда 1</value></data>
<data name="Results.Filter.Winner.Side2"><value>Команда 2</value></data>
<data name="Results.Filter.Winner.Draw"><value>Ничья</value></data>
<data name="Results.Column.Time"><value>Время</value></data>
<data name="Results.Column.Country"><value>Страна</value></data>
<data name="Results.Column.League"><value>Лига</value></data>
<data name="Results.Column.Match"><value>Матч</value></data>
<data name="Results.Column.Score"><value>Счёт</value></data>
<data name="Results.Column.Winner"><value>Победитель</value></data>
<data name="Results.Column.CompletedAt"><value>Завершено</value></data>
<data name="Results.Empty"><value>Результатов в выбранном диапазоне ещё нет. Запустите загрузку или подождите завершения матчей.</value></data>
<data name="Results.Footer.Items"><value>результатов</value></data>
<data name="Results.Loader.Kicker"><value>Загрузка</value></data>
<data name="Results.Loader.Title"><value>Загрузить результаты</value></data>
<data name="Results.Loader.Lede"><value>Опросим страницу каждого события, заберём финальный счёт и сторону-победителя. Выберите диапазон или конкретные события.</value></data>
<data name="Results.Loader.Mode"><value>Режим</value></data>
<data name="Results.Loader.Mode.AllInRange"><value>Все в диапазоне</value></data>
<data name="Results.Loader.Mode.Selected"><value>Выбранные события</value></data>
<data name="Results.Loader.Selected.Empty"><value>Все события в этом диапазоне уже имеют результат.</value></data>
<data name="Results.Loader.Selected.CountFormat"><value>{0} выбрано</value></data>
<data name="Results.Loader.Action.Load"><value>Загрузить</value></data>
<data name="Results.Loader.Action.Cancel"><value>Отменить</value></data>
<data name="Results.Loader.Action.Back"><value>Назад</value></data>
<data name="Results.Loader.Progress.Format"><value>{0} / {1}</value></data>
<data name="Results.Loader.Progress.Loaded"><value>Загружено</value></data>
<data name="Results.Loader.Progress.AlreadyLoaded"><value>Уже было</value></data>
<data name="Results.Loader.Progress.NotYetComplete"><value>Не завершено</value></data>
<data name="Results.Loader.Progress.Failed"><value>Ошибка</value></data>
<data name="Results.Loader.Summary.Format"><value>Загружено {0}, пропущено {1}, всего обработано {2}.</value></data>
<data name="Results.Loader.Empty.NoCandidates"><value>Нет событий для загрузки в этом диапазоне.</value></data>
</root>