553db2bce3
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.
62 lines
2.1 KiB
Plaintext
62 lines
2.1 KiB
Plaintext
@page "/live"
|
|
@using Marathon.UI.Pages.Shared
|
|
@implements IDisposable
|
|
@inject IStringLocalizer<SharedResource> L
|
|
@inject IEventBrowsingService Browsing
|
|
@inject EventBrowsingState BrowsingState
|
|
@inject NavigationManager Nav
|
|
@inject IOptionsMonitor<ScrapingSettingsForm> ScrapingMonitor
|
|
|
|
<PageTitle>@L["App.Title"] · @L["Nav.Live"]</PageTitle>
|
|
|
|
<EventListShell
|
|
Surface="@L["Nav.Section.Analysis"]"
|
|
Title="@L["Live.Title"]"
|
|
Lede="@L["Live.Lede"]"
|
|
Loader="@LoadAsync"
|
|
Filter="@BrowsingState.Live"
|
|
OnFilterChanged="@HandleFilterChanged"
|
|
OnRowClicked="@HandleRowClicked"
|
|
AvailableSports="_availableSports"
|
|
AvailableCountries="_availableCountries"
|
|
LiveMode="true"
|
|
AutoRefreshSeconds="@_refreshSeconds" />
|
|
|
|
@code {
|
|
private IReadOnlyList<int> _availableSports = Array.Empty<int>();
|
|
private IReadOnlyList<string> _availableCountries = Array.Empty<string>();
|
|
private int _refreshSeconds = 30;
|
|
private IDisposable? _scrapingChange;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_refreshSeconds = Math.Max(5, ScrapingMonitor.CurrentValue.PollingIntervalSeconds);
|
|
_scrapingChange = ScrapingMonitor.OnChange(opts =>
|
|
{
|
|
_refreshSeconds = Math.Max(5, opts.PollingIntervalSeconds);
|
|
InvokeAsync(StateHasChanged);
|
|
});
|
|
|
|
try
|
|
{
|
|
_availableSports = await Browsing.ListKnownSportCodesAsync(CancellationToken.None);
|
|
_availableCountries = await Browsing.ListKnownCountryCodesAsync(CancellationToken.None);
|
|
}
|
|
catch
|
|
{
|
|
// Tolerate empty data sources during early phases.
|
|
}
|
|
}
|
|
|
|
private Task<IReadOnlyList<EventListItem>> LoadAsync(EventFilter filter, CancellationToken ct)
|
|
=> Browsing.ListLiveAsync(filter, ct);
|
|
|
|
private void HandleFilterChanged(EventBrowsingState.PageFilter next)
|
|
=> BrowsingState.UpdateLive(next);
|
|
|
|
private void HandleRowClicked(EventListItem row)
|
|
=> Nav.NavigateTo($"/events/{Uri.EscapeDataString(row.Id.Value)}");
|
|
|
|
public void Dispose() => _scrapingChange?.Dispose();
|
|
}
|