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,78 @@
|
||||
@*
|
||||
SportIcon — minimal, distinctive inline-SVG icons per sport. Inline (not
|
||||
icon library) so the wordmark stays editorial-quant: thin strokes, sharp
|
||||
corners, no shadowing. Coverage matches Phase 0 spike findings:
|
||||
Basketball=6, Football=11, Tennis=22723, Hockey=43658.
|
||||
*@
|
||||
|
||||
<span class="m-sport @ClassName" role="img" aria-label="@Label" title="@Label" data-sport="@Code">
|
||||
@SvgContent
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.m-sport {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--m-sport-size, 18px);
|
||||
height: var(--m-sport-size, 18px);
|
||||
color: var(--m-sport-color, var(--m-c-ink-soft));
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.m-sport svg { width: 100%; height: 100%; display: block; }
|
||||
.m-sport[data-sport="6"] { color: #d97706; }
|
||||
.m-sport[data-sport="11"] { color: #15803d; }
|
||||
.m-sport[data-sport="22723"] { color: #0369a1; }
|
||||
.m-sport[data-sport="43658"] { color: #6d28d9; }
|
||||
[data-theme="dark"] .m-sport { filter: brightness(1.1); }
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public int Code { get; set; }
|
||||
[Parameter] public string? Label { get; set; }
|
||||
[Parameter] public string? ClassName { get; set; }
|
||||
|
||||
private MarkupString SvgContent => new(GetSvg(Code));
|
||||
|
||||
private static string GetSvg(int code) => code switch
|
||||
{
|
||||
6 => Basketball,
|
||||
11 => Football,
|
||||
22723 => Tennis,
|
||||
43658 => Hockey,
|
||||
_ => Generic,
|
||||
};
|
||||
|
||||
// SVG glyphs — single-quote attributes so the entire literal can stay on one line.
|
||||
private const string Basketball =
|
||||
"<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round'>" +
|
||||
"<circle cx='12' cy='12' r='9' />" +
|
||||
"<path d='M3 12h18 M12 3v18 M5 5c4 4 10 4 14 0 M5 19c4-4 10-4 14 0' />" +
|
||||
"</svg>";
|
||||
|
||||
private const string Football =
|
||||
"<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linejoin='round'>" +
|
||||
"<circle cx='12' cy='12' r='9' />" +
|
||||
"<polygon points='12,7 16.5,10 14.5,15 9.5,15 7.5,10' />" +
|
||||
"</svg>";
|
||||
|
||||
private const string Tennis =
|
||||
"<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round'>" +
|
||||
"<circle cx='12' cy='12' r='9' />" +
|
||||
"<path d='M3.5 9 C 9 9 15 15 20.5 15' />" +
|
||||
"<path d='M3.5 15 C 9 15 15 9 20.5 9' />" +
|
||||
"</svg>";
|
||||
|
||||
private const string Hockey =
|
||||
"<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round'>" +
|
||||
"<ellipse cx='12' cy='14' rx='8' ry='3' />" +
|
||||
"<path d='M4 14 L4 11 M20 14 L20 11' />" +
|
||||
"<path d='M6 7 L18 4' />" +
|
||||
"</svg>";
|
||||
|
||||
private const string Generic =
|
||||
"<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.6'>" +
|
||||
"<circle cx='12' cy='12' r='9' />" +
|
||||
"<circle cx='12' cy='12' r='3' />" +
|
||||
"</svg>";
|
||||
}
|
||||
Reference in New Issue
Block a user