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.
63 lines
2.7 KiB
C#
63 lines
2.7 KiB
C#
using Marathon.UI.Services;
|
|
using DomainEventId = Marathon.Domain.ValueObjects.EventId;
|
|
|
|
namespace Marathon.UI.Tests.Support;
|
|
|
|
/// <summary>
|
|
/// In-memory <see cref="IEventBrowsingService"/> for bUnit tests.
|
|
/// Seed via <see cref="UpcomingItems"/> / <see cref="LiveItems"/> / <see cref="Detail"/>.
|
|
/// </summary>
|
|
public sealed class FakeEventBrowsingService : IEventBrowsingService
|
|
{
|
|
public List<EventListItem> UpcomingItems { get; } = new();
|
|
public List<EventListItem> LiveItems { get; } = new();
|
|
public EventDetail? Detail { get; set; }
|
|
public List<int> SportCodes { get; } = new();
|
|
public List<string> CountryCodes { get; } = new();
|
|
public int UpcomingCallCount { get; private set; }
|
|
public int LiveCallCount { get; private set; }
|
|
public EventFilter? LastUpcomingFilter { get; private set; }
|
|
public EventFilter? LastLiveFilter { get; private set; }
|
|
|
|
public Task<IReadOnlyList<EventListItem>> ListUpcomingAsync(EventFilter filter, CancellationToken ct)
|
|
{
|
|
UpcomingCallCount++;
|
|
LastUpcomingFilter = filter;
|
|
return Task.FromResult<IReadOnlyList<EventListItem>>(Apply(UpcomingItems, filter));
|
|
}
|
|
|
|
public Task<IReadOnlyList<EventListItem>> ListLiveAsync(EventFilter filter, CancellationToken ct)
|
|
{
|
|
LiveCallCount++;
|
|
LastLiveFilter = filter;
|
|
return Task.FromResult<IReadOnlyList<EventListItem>>(Apply(LiveItems, filter));
|
|
}
|
|
|
|
public Task<EventDetail?> GetDetailAsync(DomainEventId eventId, CancellationToken ct)
|
|
=> Task.FromResult(Detail);
|
|
|
|
public Task<IReadOnlyList<int>> ListKnownSportCodesAsync(CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<int>>(SportCodes);
|
|
|
|
public Task<IReadOnlyList<string>> ListKnownCountryCodesAsync(CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<string>>(CountryCodes);
|
|
|
|
private static IReadOnlyList<EventListItem> Apply(IEnumerable<EventListItem> source, EventFilter filter)
|
|
{
|
|
IEnumerable<EventListItem> q = source;
|
|
if (filter.SportCodes is { Count: > 0 } sports)
|
|
q = q.Where(e => sports.Contains(e.Sport.Value));
|
|
if (filter.CountryCodes is { Count: > 0 } countries)
|
|
q = q.Where(e => countries.Contains(e.CountryCode, StringComparer.OrdinalIgnoreCase));
|
|
if (!string.IsNullOrWhiteSpace(filter.SearchTerm))
|
|
{
|
|
var t = filter.SearchTerm.Trim();
|
|
q = q.Where(e =>
|
|
e.LeagueId.Contains(t, StringComparison.OrdinalIgnoreCase) ||
|
|
e.Side1Name.Contains(t, StringComparison.OrdinalIgnoreCase) ||
|
|
e.Side2Name.Contains(t, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
return q.ToList();
|
|
}
|
|
}
|