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:
@@ -0,0 +1,104 @@
|
||||
@*
|
||||
OddsCell — tabular mono rendering of a decimal odds rate, with an inline
|
||||
direction marker (▲ amber rising, ▼ red falling, em-dash unchanged) when
|
||||
the value differs from a previous render. Drives the visual indicator
|
||||
that Phase 6 promises for live-list odds movement.
|
||||
|
||||
Rate is the current value. Previous (optional) is the prior value the
|
||||
caller wants compared against. Caller decides what "previous" means
|
||||
(last refresh / last snapshot) and tracks it in its own state.
|
||||
*@
|
||||
|
||||
@{
|
||||
var direction = Direction;
|
||||
var directionGlyph = direction switch
|
||||
{
|
||||
Trend.Up => "▲", // ▲
|
||||
Trend.Down => "▼", // ▼
|
||||
_ => "—", // em-dash
|
||||
};
|
||||
var directionClass = direction switch
|
||||
{
|
||||
Trend.Up => "is-up",
|
||||
Trend.Down => "is-down",
|
||||
_ => "is-flat",
|
||||
};
|
||||
var ariaTrend = direction switch
|
||||
{
|
||||
Trend.Up => "rising",
|
||||
Trend.Down => "falling",
|
||||
_ => "unchanged",
|
||||
};
|
||||
}
|
||||
|
||||
<span class="m-odds @directionClass" aria-label="@AriaPrefix @Formatted (@ariaTrend)" data-trend="@ariaTrend">
|
||||
<span class="m-odds__value m-mono" data-numeric>@Formatted</span>
|
||||
@if (ShowTrend)
|
||||
{
|
||||
<span class="m-odds__delta" aria-hidden="true">@directionGlyph</span>
|
||||
}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.m-odds {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 56px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.m-odds__value {
|
||||
font-feature-settings: "tnum" 1, "lnum" 1;
|
||||
font-weight: 500;
|
||||
color: var(--m-c-ink);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
.m-odds__delta {
|
||||
font-family: var(--m-font-mono);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1;
|
||||
color: var(--m-c-ink-soft);
|
||||
transition: color 220ms ease;
|
||||
}
|
||||
.m-odds.is-up .m-odds__delta { color: var(--m-c-accent); }
|
||||
.m-odds.is-down .m-odds__delta { color: var(--m-c-anomaly); }
|
||||
.m-odds.is-flat .m-odds__delta { color: var(--m-c-ink-soft); }
|
||||
|
||||
.m-odds.is-up .m-odds__value,
|
||||
.m-odds.is-down .m-odds__value {
|
||||
animation: m-odds-flash 1200ms ease-out 1;
|
||||
}
|
||||
|
||||
@@keyframes m-odds-flash {
|
||||
0% { background: color-mix(in srgb, currentColor 14%, transparent); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
|
||||
@@media (prefers-reduced-motion: reduce) {
|
||||
.m-odds.is-up .m-odds__value,
|
||||
.m-odds.is-down .m-odds__value { animation: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public decimal? Rate { get; set; }
|
||||
[Parameter] public decimal? Previous { get; set; }
|
||||
[Parameter] public bool ShowTrend { get; set; } = true;
|
||||
[Parameter] public string AriaPrefix { get; set; } = "Odds";
|
||||
[Parameter] public string EmptyPlaceholder { get; set; } = "—";
|
||||
|
||||
private string Formatted => Rate is { } r ? r.ToString("0.00") : EmptyPlaceholder;
|
||||
|
||||
private Trend Direction
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Rate is not { } r || Previous is not { } p) return Trend.Flat;
|
||||
if (r > p) return Trend.Up;
|
||||
if (r < p) return Trend.Down;
|
||||
return Trend.Flat;
|
||||
}
|
||||
}
|
||||
|
||||
private enum Trend { Flat, Up, Down }
|
||||
}
|
||||
Reference in New Issue
Block a user