@* AnomalyCard — single row in the anomaly feed. Asymmetric layout: severity badge at top-right, sport icon + event title at top-left, a compact pre→post odds strip in the middle, the detected-at timestamp at the bottom. Whole card is clickable / Enter/Space-keyable to navigate to /anomalies/{id}. Visual tone shifts with severity: - High: signal-red left border + paper-2 background. - Medium: amber left border. - Low: muted neutral border. *@ @using Marathon.UI.Components @inject IStringLocalizer L
@KindLabel(Item.Kind) · @Item.CountryCode · @Item.LeagueId

@Item.EventTitle

@L["Anomaly.Card.DetectedAt"] @L["Anomaly.Card.GapSeconds"] · @FormatGap(Item.SuspensionGapSeconds)
@code { [Parameter, EditorRequired] public AnomalyListItem Item { get; set; } = default!; [Parameter] public EventCallback OnClick { get; set; } private string _severityClass => Item.Severity switch { AnomalySeverity.High => "high", AnomalySeverity.Medium => "medium", _ => "low", }; private RenderFragment RenderRateCell(string label, decimal? pre, decimal? post) => builder => { builder.OpenElement(0, "span"); builder.AddAttribute(1, "class", "m-anomaly-card__rate"); builder.OpenElement(2, "span"); builder.AddAttribute(3, "class", "m-anomaly-card__rate-label"); builder.AddContent(4, label); builder.CloseElement(); builder.OpenElement(5, "span"); builder.AddAttribute(6, "class", "m-anomaly-card__rate-pre"); builder.AddContent(7, FormatRate(pre)); builder.CloseElement(); builder.OpenElement(8, "span"); builder.AddAttribute(9, "class", "m-anomaly-card__rate-arrow"); builder.AddContent(10, "→"); builder.CloseElement(); builder.OpenElement(11, "span"); builder.AddAttribute(12, "class", "m-anomaly-card__rate-post"); builder.AddContent(13, FormatRate(post)); builder.CloseElement(); builder.CloseElement(); }; private async Task HandleClick() { if (OnClick.HasDelegate) await OnClick.InvokeAsync(Item); } private async Task HandleKey(KeyboardEventArgs e) { if (e.Key == "Enter" || e.Key == " ") { if (OnClick.HasDelegate) await OnClick.InvokeAsync(Item); } } private string KindLabel(AnomalyKind kind) => kind switch { AnomalyKind.SuspensionFlip => L["Anomaly.Kind.SuspensionFlip"], AnomalyKind.SteamMove => L["Anomaly.Kind.SteamMove"], _ => kind.ToString(), }; private string SportLabel(int code) => SportLabels.Resolve(L, code); private static string FormatRate(decimal? r) => r is { } v ? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) : "—"; private static string FormatGap(int seconds) { if (seconds <= 0) return "—"; var ts = TimeSpan.FromSeconds(seconds); if (ts.TotalSeconds < 60) return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}s", (int)ts.TotalSeconds); if (ts.TotalMinutes < 60) return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}m{1:00}s", (int)ts.TotalMinutes, ts.Seconds); return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}h{1:00}m", (int)ts.TotalHours, ts.Minutes); } private static string FormatRelative(DateTimeOffset value) { var delta = DateTimeOffset.UtcNow - value; if (delta < TimeSpan.Zero) delta = TimeSpan.Zero; if (delta.TotalSeconds < 60) return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}s ago", (int)delta.TotalSeconds); if (delta.TotalMinutes < 60) return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}m ago", (int)delta.TotalMinutes); if (delta.TotalHours < 24) return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}h ago", (int)delta.TotalHours); return value.ToString("dd MMM HH:mm", System.Globalization.CultureInfo.InvariantCulture); } }