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.
This commit is contained in:
2026-05-28 23:08:56 +03:00
parent 2b1025cae3
commit d9d92ea8fd
7 changed files with 103 additions and 0 deletions
@@ -54,6 +54,13 @@
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>
@@ -99,6 +106,7 @@
private string KindLabel(AnomalyKind kind) => kind switch
{
AnomalyKind.SuspensionFlip => L["Anomaly.Kind.SuspensionFlip"],
AnomalyKind.SteamMove => L["Anomaly.Kind.SteamMove"],
_ => kind.ToString(),
};
@@ -114,6 +114,20 @@
<article class="m-card m-card--accented m-journal__form-card">
<div class="m-journal__form-grid">
<div class="m-journal__form-field m-journal__form-field--wide">
<label class="m-journal__form-label">@L["Journal.Field.FindEvent"]</label>
<MudAutocomplete T="EventOption"
SearchFunc="SearchEventsAsync"
ToStringFunc="@(o => o is null ? string.Empty : o.Label)"
ValueChanged="OnEventSelected"
Variant="Variant.Outlined"
Clearable="true"
ResetValueOnEmptyText="true"
Placeholder="@L["Journal.Field.FindEvent.Placeholder"]"
data-test="journal-find-event" />
<span class="m-journal__form-hint">@L["Journal.Field.FindEvent.Hint"]</span>
</div>
<div class="m-journal__form-field m-journal__form-field--wide">
<label class="m-journal__form-label" for="journal-event-id">@L["Journal.Field.EventId"]</label>
<MudTextField id="journal-event-id"
@@ -718,9 +732,48 @@
}
private CancellationTokenSource? _loadCts;
private IReadOnlyList<EventOption> _eventOptions = Array.Empty<EventOption>();
/// <summary>Optional event code supplied by a "Log bet" deep link (e.g. from an anomaly).</summary>
[Parameter, SupplyParameterFromQuery(Name = "eventId")]
public string? PrefillEventId { get; set; }
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrWhiteSpace(PrefillEventId))
_form.EventId = PrefillEventId.Trim();
await LoadAsync();
await LoadEventOptionsAsync();
}
private async Task LoadEventOptionsAsync()
{
try
{
var options = await Service.GetUpcomingEventOptionsAsync(_loadCts?.Token ?? CancellationToken.None);
_eventOptions = options ?? Array.Empty<EventOption>();
}
catch (Exception ex)
{
// The autocomplete is a convenience; failing to populate it must not break the page.
Logger.LogWarning(ex, "Journal: failed to load event options for the autocomplete.");
_eventOptions = Array.Empty<EventOption>();
}
}
private Task<IEnumerable<EventOption>> SearchEventsAsync(string? value, CancellationToken token)
{
IEnumerable<EventOption> matches = string.IsNullOrWhiteSpace(value)
? _eventOptions
: _eventOptions.Where(o => o.Label.Contains(value, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(matches.Take(20));
}
private void OnEventSelected(EventOption? option)
{
if (option is not null)
_form.EventId = option.Id;
}
private async Task LoadAsync()