Files
maraphon-app/src/Marathon.UI/Pages/Anomalies/Detail.razor
T
alexei.dolgolyov d9d92ea8fd feat(ui): event autocomplete + log-bet deep link, steam-move label
- 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.
2026-05-28 23:08:56 +03:00

122 lines
5.2 KiB
Plaintext

@page "/anomalies/{Id:guid}"
@using Marathon.UI.Components
@inject IStringLocalizer<SharedResource> L
@inject IAnomalyBrowsingService Anomalies
@inject NavigationManager Nav
<PageTitle>@L["App.Title"] · @L["Anomaly.Title"]</PageTitle>
<section class="m-shell">
@if (_loading && _detail is null)
{
<div class="m-list-empty">
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
<span class="m-mono">@L["Common.Loading"]</span>
</div>
}
else if (_detail is null)
{
<div class="m-list-empty">
<span class="m-kicker" style="border-color: var(--m-c-ink-soft); color: var(--m-c-ink-soft);">404</span>
<p style="color: var(--m-c-ink-soft);">@L["Anomaly.Detail.NotFound"]</p>
<MudButton Variant="Variant.Outlined" OnClick='() => Nav.NavigateTo("/anomalies")'>
@L["Anomaly.Detail.BackToFeed"]
</MudButton>
</div>
}
else
{
<header class="m-detail-header m-rise m-rise-1">
<div class="m-detail-header__lockup">
<span class="m-kicker" style="color: var(--m-c-anomaly); border-color: var(--m-c-anomaly);">
@KindLabel(_detail.Item.Kind) · @_detail.Item.CountryCode · @_detail.Item.LeagueId
</span>
<h1 class="m-display" style="font-size: clamp(1.75rem, 3vw, 2.5rem); margin-top: var(--m-space-2);">
@_detail.Item.EventTitle
</h1>
<div class="m-mono" style="margin-top: var(--m-space-2); color: var(--m-c-ink-soft); text-transform: uppercase; letter-spacing: 0.14em; font-size: 0.75rem;">
@L["Anomaly.Card.DetectedAt"] @_detail.Item.DetectedAt.ToString("dd MMM yyyy · HH:mm:ss") · MSK
</div>
</div>
<aside class="m-detail-header__odds">
<div class="m-detail-header__odds-row">
<span class="m-detail-header__odds-label">@L["Anomaly.Card.Score"]</span>
<SeverityBadge Severity="_detail.Item.Severity" Score="_detail.Item.Score" />
</div>
<div class="m-detail-header__odds-row">
<span class="m-detail-header__odds-label">@L["Anomaly.Card.GapSeconds"]</span>
<span class="m-mono" data-test="suspension-duration">@FormatGap(_detail.Item.SuspensionGapSeconds)</span>
</div>
<MudButton Variant="Variant.Outlined"
StartIcon="@Icons.Material.Outlined.OpenInNew"
OnClick="@(() => Nav.NavigateTo($"/events/{Uri.EscapeDataString(_detail.Item.EventId.Value)}"))"
Class="m-detail-header__export"
data-test="link-back-to-event">
@L["Anomaly.Detail.LinkBackToEvent"]
</MudButton>
<MudButton Variant="Variant.Outlined"
StartIcon="@Icons.Material.Outlined.Receipt"
OnClick="@(() => Nav.NavigateTo($"/my-bets?eventId={Uri.EscapeDataString(_detail.Item.EventId.Value)}"))"
Class="m-detail-header__export"
data-test="log-bet">
@L["Action.LogBet"]
</MudButton>
</aside>
</header>
<hr class="m-rule" />
<article class="m-card m-card--anomaly m-rise m-rise-2">
<span class="m-kicker" style="color: var(--m-c-anomaly); border-color: var(--m-c-anomaly);">
@L["Anomaly.Detail.EvidenceTitle"]
</span>
<div style="margin-top: var(--m-space-4);">
<AnomalyEvidence Pre="_detail.Pre"
Post="_detail.Post"
SuspensionGapSeconds="_detail.Item.SuspensionGapSeconds"
IsTwoWay="_detail.Item.IsTwoWay" />
</div>
</article>
}
</section>
@code {
[Parameter] public Guid Id { get; set; }
private AnomalyDetailVm? _detail;
private bool _loading = true;
protected override async Task OnParametersSetAsync()
{
_loading = true;
try
{
_detail = await Anomalies.GetByIdAsync(Id, CancellationToken.None);
}
catch
{
_detail = null;
}
finally
{
_loading = false;
}
}
private string KindLabel(AnomalyKind kind) => kind switch
{
AnomalyKind.SuspensionFlip => L["Anomaly.Kind.SuspensionFlip"],
AnomalyKind.SteamMove => L["Anomaly.Kind.SteamMove"],
_ => kind.ToString(),
};
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);
}
}