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.
105 lines
3.1 KiB
Plaintext
105 lines
3.1 KiB
Plaintext
@*
|
|
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 }
|
|
}
|