d9d92ea8fd
- MyBets: add a "Find event" MudAutocomplete over upcoming events (loaded once, filtered client-side) that fills the Event ID; the manual ID field stays as a fallback. Backed by IBetJournalService.GetUpcomingEventOptionsAsync. - Add a "Log bet" CTA on the anomaly detail page that deep-links to /my-bets?eventId=<code>; the journal prefills the Event ID from the query. - Render the new SteamMove anomaly kind with a localized label in the card and detail KindLabel switches (was falling through to the raw enum name). - Localization (en+ru) for all new strings.
244 lines
8.7 KiB
Plaintext
244 lines
8.7 KiB
Plaintext
@*
|
|
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<SharedResource> L
|
|
|
|
<article class="m-anomaly-card m-anomaly-card--@_severityClass m-rise"
|
|
role="link"
|
|
tabindex="0"
|
|
data-test="anomaly-card"
|
|
data-anomaly-id="@Item.Id"
|
|
@onclick="HandleClick"
|
|
@onkeydown="HandleKey">
|
|
<header class="m-anomaly-card__head">
|
|
<div class="m-anomaly-card__lockup">
|
|
<SportIcon Code="@Item.Sport.Value" Label="@SportLabel(Item.Sport.Value)" ClassName="m-anomaly-card__sport" />
|
|
<div class="m-anomaly-card__title-block">
|
|
<span class="m-kicker">@KindLabel(Item.Kind) · @Item.CountryCode · @Item.LeagueId</span>
|
|
<h3 class="m-anomaly-card__title">@Item.EventTitle</h3>
|
|
</div>
|
|
</div>
|
|
<SeverityBadge Severity="Item.Severity" Score="Item.Score" />
|
|
</header>
|
|
|
|
<div class="m-anomaly-card__strip" aria-hidden="true">
|
|
@RenderRateCell(L["Detail.Chart.Win1"], Item.PreWin1Rate, Item.PostWin1Rate)
|
|
@if (!Item.IsTwoWay)
|
|
{
|
|
@RenderRateCell(L["Detail.Chart.Draw"], Item.PreDrawRate, Item.PostDrawRate)
|
|
}
|
|
@RenderRateCell(L["Detail.Chart.Win2"], Item.PreWin2Rate, Item.PostWin2Rate)
|
|
</div>
|
|
|
|
<footer class="m-anomaly-card__foot">
|
|
<span class="m-mono m-anomaly-card__time">
|
|
<span class="m-anomaly-card__time-label">@L["Anomaly.Card.DetectedAt"]</span>
|
|
<time datetime="@Item.DetectedAt.ToString("o")" title="@Item.DetectedAt.ToString("dd MMM yyyy HH:mm:ss")">
|
|
@FormatRelative(Item.DetectedAt)
|
|
</time>
|
|
</span>
|
|
<span class="m-mono m-anomaly-card__gap">
|
|
@L["Anomaly.Card.GapSeconds"] · @FormatGap(Item.SuspensionGapSeconds)
|
|
</span>
|
|
</footer>
|
|
</article>
|
|
|
|
<style>
|
|
.m-anomaly-card {
|
|
display: grid;
|
|
gap: var(--m-space-3);
|
|
padding: var(--m-space-4) var(--m-space-5);
|
|
background: var(--m-c-paper);
|
|
border: 1px solid var(--m-c-rule);
|
|
border-left: 3px solid var(--m-c-rule);
|
|
cursor: pointer;
|
|
transition: background 140ms ease, border-color 140ms ease, transform 140ms ease;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
.m-anomaly-card:hover {
|
|
background: var(--m-c-paper-2);
|
|
transform: translateX(2px);
|
|
}
|
|
.m-anomaly-card:focus-visible {
|
|
outline: 2px solid var(--m-c-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
.m-anomaly-card--high { border-left-color: var(--m-c-anomaly); }
|
|
.m-anomaly-card--medium { border-left-color: var(--m-c-accent); }
|
|
.m-anomaly-card--low { border-left-color: var(--m-c-ink-soft); }
|
|
|
|
.m-anomaly-card__head {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: var(--m-space-3);
|
|
align-items: start;
|
|
}
|
|
.m-anomaly-card__lockup {
|
|
display: flex;
|
|
gap: var(--m-space-3);
|
|
align-items: flex-start;
|
|
}
|
|
.m-anomaly-card__sport {
|
|
--m-sport-size: 22px;
|
|
margin-top: 4px;
|
|
}
|
|
.m-anomaly-card__title-block { display: grid; gap: 4px; min-width: 0; }
|
|
.m-anomaly-card__title {
|
|
margin: 0;
|
|
font-family: var(--m-font-display);
|
|
font-weight: 400;
|
|
font-size: 1.125rem;
|
|
line-height: 1.25;
|
|
color: var(--m-c-ink);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.m-anomaly-card__strip {
|
|
display: flex;
|
|
gap: var(--m-space-3);
|
|
flex-wrap: wrap;
|
|
padding: var(--m-space-3);
|
|
background: var(--m-c-paper-2);
|
|
border: 1px solid var(--m-c-rule);
|
|
}
|
|
.m-anomaly-card__rate {
|
|
display: grid;
|
|
grid-template-columns: auto auto auto auto;
|
|
gap: 8px;
|
|
align-items: baseline;
|
|
font-family: var(--m-font-mono);
|
|
}
|
|
.m-anomaly-card__rate-label {
|
|
font-size: 0.6875rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.16em;
|
|
color: var(--m-c-ink-soft);
|
|
}
|
|
.m-anomaly-card__rate-pre {
|
|
color: var(--m-c-ink-soft);
|
|
font-size: 0.875rem;
|
|
}
|
|
.m-anomaly-card__rate-arrow { color: var(--m-c-accent); font-size: 0.875rem; }
|
|
.m-anomaly-card__rate-post {
|
|
color: var(--m-c-ink);
|
|
font-weight: 600;
|
|
font-size: 0.9375rem;
|
|
}
|
|
.m-anomaly-card--high .m-anomaly-card__rate-post {
|
|
color: var(--m-c-anomaly);
|
|
}
|
|
|
|
.m-anomaly-card__foot {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: var(--m-space-3);
|
|
flex-wrap: wrap;
|
|
font-size: 0.6875rem;
|
|
color: var(--m-c-ink-soft);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.14em;
|
|
}
|
|
.m-anomaly-card__time-label { margin-right: 6px; opacity: 0.7; }
|
|
.m-anomaly-card__time time { color: var(--m-c-ink); font-weight: 500; }
|
|
</style>
|
|
|
|
@code {
|
|
[Parameter, EditorRequired] public AnomalyListItem Item { get; set; } = default!;
|
|
[Parameter] public EventCallback<AnomalyListItem> 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);
|
|
}
|
|
}
|