# Phase 6: Event Browsing UI **Status:** ✅ Done **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** frontend **Implementer:** Opus + frontend-design skill **Depends on:** Phase 4 (use cases) + Phase 5 (UI shell) ## Objective Build the user-facing browsing experience: pre-match list, live list (auto-refreshing), event-detail view with odds-over-time chart, plus an Excel export trigger. Visual quality must match the design system established in Phase 5 — distinctive, accessible, information-dense without being cluttered. ## Tasks - [x] Create `Marathon.UI/Pages/PreMatch.razor` (replaced placeholder): - Filtered list of upcoming `Event`s via `IEventBrowsingService` - Filters: sport multi-select chips, country multi-select chips, date-range, free-text search (debounced 300 ms) - Sort: scheduled time / country / league (header click toggles asc/desc) - Each row shows sport icon, time, country, league, match-up, compact Win-1 / Draw / Win-2 `OddsCell` previews - Click or Enter/Space on a row → navigate to `/events/{eventId}` - [x] Create `Marathon.UI/Pages/Live.razor` (replaced placeholder): - Same shell (`Pages/Shared/EventListShell.razor`) as PreMatch but data source is live snapshots - Auto-refresh every `Scraping:PollingIntervalSeconds`, read live via `IOptionsMonitor`; pulse badge in toolbar surfaces the active cadence - Visual indicator when odds change since last refresh (▲ amber rising, ▼ red falling, em-dash unchanged + flash background) - [x] Create `Marathon.UI/Pages/Events/Detail.razor`: - Event header: sport kicker, sides 1 & 2 lockup, scheduled time + MSK, Win-1 / Draw / Win-2 odds cluster, Export button - Tabs: "Match" + dynamic "Period 1..N" generated from snapshot data - Per scope: Type / Side / Threshold / Rate table for all bets - Charts panel: `OddsTimeline` wraps Plotly.Blazor (Win-1 / Draw / Win-2 traces, theme-aware colors, accessible `
` data table fallback) - Snapshot history table beneath the chart (dd MMM HH:mm:ss + Source + rates + bet count) - Excel export button → opens `ExportDialog`, success snackbar with path - [x] Create `Marathon.UI/Components/SportIcon.razor` — inline SVG icons per sport (basketball=6, football=11, tennis=22723, hockey=43658, generic fallback) - [x] Create `Marathon.UI/Components/OddsCell.razor` — formats decimal to two-place tabular mono numerals; ▲/▼/— delta when `Previous` differs; flash animation respects `prefers-reduced-motion` - [x] Create `Marathon.UI/Components/OddsTimeline.razor` — wraps Plotly.Blazor with editorial-quant theming (parchment paper-bg light / ink-near-black dark, navy / amber / signal-red trace colors, mono tick fonts) plus a hidden `
` data table for screen readers; memoizes traces on signature change - [x] Create `Marathon.UI/Components/ExportDialog.razor` — modal: From/To date pickers + `ExportKind` radio group + Export button → calls `ExportToExcelUseCase`. Esc cancels, Enter submits. Shows error inline when validation fails or the use case throws. - [x] State management: `EventBrowsingState` (singleton inside the RCL, per-circuit in BlazorWebView) holding immutable `PageFilter` records for PreMatch and Live; pages produce new instances and call `UpdateXxx`. `OnChange` event for subscribers. - [x] Add `Plotly.Blazor` 5.4.1 to `Directory.Packages.props` and `Marathon.UI.csproj` - [x] Append all new strings to `SharedResource.ru.resx` + `SharedResource.en.resx` using the Phase 5 dot-segmented convention (`PreMatch.*`, `Live.*`, `Detail.*`, `Detail.Chart.*`, `Detail.History.*`, `Export.*`, `Sport.*`) - [x] Performance: - Filter inputs debounced 300 ms via `CancellationTokenSource` rerun guard - Chart data memoized via `_signature` (rebuild only on count / first / last timestamp / first / last rate change) - Single in-memory list per page; small enough to skip virtualization at Phase 6 scale; `` is overflow-x scrollable - [x] Accessibility: - Tables use `` / `
`; sortable headers expose ▲/▼ glyphs - Rows are `tabindex="0"` and respond to Enter/Space via `@onkeydown` - Visible amber focus rings (inherited from Phase 5 `:focus-visible` rule) - `OddsTimeline` exposes a hidden but expandable `
`/`` parallel data table for screen readers - Toolbar has `role="toolbar" aria-label`, chips have `aria-pressed` ## Files to Modify/Create - `src/Marathon.UI/Pages/PreMatch/EventsList.razor` - `src/Marathon.UI/Pages/Live/LiveList.razor` - `src/Marathon.UI/Pages/Events/Detail.razor` - `src/Marathon.UI/Components/SportIcon.razor`, `OddsCell.razor`, `OddsTimeline.razor`, `ExportDialog.razor` - `src/Marathon.UI/Services/EventBrowsingState.cs` - `src/Marathon.UI/Resources/SharedResource.{ru,en}.resx` — append new keys - `src/Marathon.UI/Components/_Imports.razor` — register Plotly.Blazor - Tests: `tests/Marathon.UI.Tests/Pages/**`, `Components/**` ## Acceptance Criteria - Compiles (Big Bang). - Live list visually conveys odds changes between refreshes. - Detail page chart renders 3 traces (Win-1/Draw/Win-2) with smooth interpolation and clear tooltip showing exact rate at any point in time. - Excel export from the dialog reaches `ExportToExcelUseCase` correctly. - Both RU and EN render correctly across all new UI. - Distinctive visual identity — implementer should follow frontend-design guidance. ## Notes - The frontend-design skill content is provided to the agent in `FRONTEND_DESIGN_SKILL`. Apply its principles — typography, color, motion, spatial composition. - Use Plotly.Blazor for charts (smooth, themable, professional look). - Keep components small (<200 lines) and composable. - Big Bang: compile-only smoke check. ## Review Checklist - [x] Compiles (full solution clean — 0 errors, 0 warnings) - [x] No mutation of domain types in UI components — pages bind to view-model records (`EventListItem`, `EventDetail`, `EventScopeBoard`, `BetRow`, `OddsTimelinePoint`, `SnapshotHistoryEntry`) shaped in `EventBrowsingService` - [x] Filters/sort persist within page session via `EventBrowsingState` - [x] Chart accessible — `
` data table fallback in `OddsTimeline` - [x] All new strings localized in RU + EN with full key parity - [x] Visual consistency with Phase 5 theme tokens — every color comes from `--m-c-*` CSS vars or the Mud palette; no new hex literals ## Test results - `dotnet build Marathon.sln`: ✅ 0 errors / 0 warnings - `dotnet test Marathon.sln`: ✅ 228 passed / 0 failed (Domain 96 + Application 15 + Infrastructure 80 + UI 37; baseline was 202, +26 new bUnit tests) ## Handoff to Next Phase ### Component patterns Phase 7 (Anomaly UI) should reuse | Pattern | File | Rationale | |---|---|---| | Section shell | `Pages/Shared/EventListShell.razor` | Header (kicker + display title + lede), `m-list-toolbar`, `m-list-table`. Anomaly feed should mimic the toolbar / chips / table cadence so the surfaces feel like a series. | | Compact data table | `m-table` class block in `EventListShell.razor` | Mono uppercase headers, `m-table__row` hover + `tabindex` keyboard-affordance pattern, `
` semantics. | | Editorial header | `Pages/Events/Detail.razor` `.m-detail-header` grid | Asymmetric 1.5fr/1fr lockup with kicker + display title + dateline on the left, summary card on the right. Ideal for an anomaly detail page. | | Tab strip | `.m-detail-tabs` block in `Detail.razor` | Sharp underline + amber accent active state. Anomaly detail can reuse for "Timeline" / "Evidence" / "Reasoning". | | Asymmetric content grid | `.m-detail-grid` (1.2fr / 1fr) | Pair a primary content card with an aside summary. | | Trend indicator | `Components/OddsCell.razor` | Anomaly UI's "movement at suspension" cell can drop in `OddsCell` directly; the `Previous` parameter accepts any prior value. | | Sport branding | `Components/SportIcon.razor` | Single source of sport visual language. Add new sports here, not ad-hoc. | | Modal pattern | `Components/ExportDialog.razor` | `MudDialog` + kicker title + grid form body + Cancel/Submit action row + inline `m-export-dialog__error` for validation errors. Anomaly UI may adopt the same shape for "Acknowledge" / "Mark false positive" dialogs. | | Plotly wrapper | `Components/OddsTimeline.razor` | Editorial-quant chart theme (paper-bg, mono tick fonts, navy / amber / signal-red accents). Anomaly chart should reuse the layout factory (or call into `OddsTimeline` directly with `Points` from the suspension window). | ### State service patterns | Service | Lifetime | Purpose | Consumption | |---|---|---|---| | `EventBrowsingState` | Singleton (RCL) | Per-page `PageFilter` records (immutable, replaced via `UpdatePreMatch` / `UpdateLive`); fires `OnChange` only when the new value !equals the old one. | Pages inject + bind via `@inject EventBrowsingState`. | | `IEventBrowsingService` → `EventBrowsingService` | Scoped | Repository facade returning view-model records (no EF graphs). Owns sort + in-memory filtering, latest-snapshot odds extraction, scope grouping. | Pages inject + call `ListUpcomingAsync`/`ListLiveAsync`/`GetDetailAsync`. | Phase 7 should follow the same shape: an `AnomalyBrowsingState` singleton + an `IAnomalyBrowsingService` scoped facade that returns `AnomalyListItem` view-models with no `Anomaly` domain leakage. ### Localization key naming Phase 6 followed Phase 5's convention strictly (dot-segmented `.`): - `PreMatch.*` — pre-match list page (`PreMatch.Title`, `PreMatch.Filter.From`, `PreMatch.Column.Time`, `PreMatch.Footer.Events`, `PreMatch.Empty`) - `Live.*` — live list page (`Live.Title`, `Live.AutoRefresh`, `Live.Lede`) - `Detail.*` — event detail page (`Detail.Title`, `Detail.Tabs.Match`, `Detail.Tabs.Period` with `{0}` placeholder, `Detail.BetType.*`, `Detail.Side.*`, `Detail.Chart.*`, `Detail.Chart.AccessibleSummary`, `Detail.History.Title`, `Detail.History.Source`, `Detail.History.Live`, `Detail.History.PreMatch`) - `Export.*` — export dialog (`Export.Title`, `Export.DateRange.From`, `Export.Kind.PreMatch|Live|Combined`, `Export.Submit`, `Export.Cancel`, `Export.Success` with `{0}` placeholder for path, `Export.Error.MissingDates|InvalidRange|Failed`) - `Sport.*` — sport display names (`Sport.Basketball`, `Sport.Football`, `Sport.Tennis`, `Sport.Hockey`) Phase 7 strings should slot under `Anomaly.*` (the `Anomaly.Live` / `Anomaly.Kind.SuspensionFlip` / `Anomaly.Score` keys are already reserved from Phase 5). ### Routing additions - `/prematch` (existing — body replaced) - `/live` (existing — body replaced) - `/events/{EventCode}` (new) — accepts a URL-escaped `EventId.Value` (numeric for marathonbet.by; allow non-numeric for forward compatibility) Phase 7 should add `/anomalies/{eventId}` or `/anomalies/{anomalyId}` and link to the matching detail page from the home dashboard's "Latest signals" feed. ### Theme + Plotly tokens - Plotly traces use the same triplet as the rest of the app: navy `#0f172a` for Win-1, amber `#d97706` for Draw, signal-red `#dc2626` for Win-2. Phase 7 can reuse the same trace palette for "before suspension" / "during suspension" / "after suspension" (with red as the alert tone — this is load-bearing). - Plotly.Blazor 5.4.1 is on the .NET 8 line; staying on this major avoids the v7 breaking changes documented upstream. Phase 7's anomaly chart should call into `OddsTimeline` if possible, only forking if it needs additional axes or annotations (e.g. a vertical band for the suspension window). ### Verified invariants & gotchas - `Marathon.UI` still references **only** Domain + Application + framework packages. `Plotly.Blazor` was added; it's an MIT-licensed Razor wrapper with no Infrastructure / Hosting deps, so the RCL stays host-agnostic. - `DateRange` ambiguity: both `MudBlazor.DateRange` and `Marathon.Application.Storage.DateRange` are visible inside Razor pages that import both namespaces (via `_Imports.razor`). Use `using AppDateRange = Marathon.Application.Storage.DateRange;` in any file that calls the application's `DateRange`. Already applied in `ExportDialog.razor` and `ExportDialogTests.cs`. - Razor source generator does not accept C# 11 raw string literals (`"""..."""`) inside `@code` blocks — the parser sees the leading `"""` as the start of a normal string and never finds the close. Use concatenated single-quoted attribute SVG strings instead (see `SportIcon.razor`). - `code` is reserved by the Razor source generator. Loop over a list with any other identifier (`@foreach (var sportCode in ...)`). - `Plotly.Blazor` exposes a `Plotly.Blazor.LayoutLib.Margin` that conflicts with `MudBlazor.Margin`. Fully qualify the layout-side type as `new Plotly.Blazor.LayoutLib.Margin {...}`. ### Test infrastructure delta (for Phase 7) - `tests/Marathon.UI.Tests/Support/MarathonTestContext` now also registers a `FakeEventBrowsingService` and `EventBrowsingState` singleton; Phase 7 tests can reuse both, or follow the same fake pattern for an `IAnomalyBrowsingService`. - `Support/TestData.cs` exposes `MoscowToday(int hour)`, `ListItem(...)`, and `Detail(...)` factories; reuse for anomaly fixtures. - `Support/TestOptionsMonitor` wraps `IOptionsMonitor` for tests that need to drive options-change callbacks deterministically.