feat(anomalies): sort the feed (newest / top score / longest gap)

Adds a sort chip row to the anomaly feed — newest (default), highest score, or
longest suspension gap — replacing the fixed newest-first order. DetectedAt is the
tiebreak so order stays stable.

- AnomalySort enum + AnomalyFilter.Sort (default Newest, so existing constructions
  are unaffected); AnomalyBrowsingService applies it; feed sort chips +
  SetSort/SortLabel; en/ru resx.
- 2 tests: default newest-first + highest-score ordering.
This commit is contained in:
2026-05-29 11:55:46 +03:00
parent 67f2ae130c
commit 36178e6d1b
6 changed files with 93 additions and 7 deletions
@@ -99,6 +99,23 @@
}
</div>
<div class="m-list-toolbar__row m-list-toolbar__chips">
<span class="m-list-toolbar__label">@L["Anomaly.Filter.Sort"]</span>
@foreach (var sort in _sortOptions)
{
var localSort = sort;
var active = _filter.Sort == localSort;
<button type="button"
class="m-chip @(active ? "is-active" : null)"
aria-pressed="@active"
data-test="sort-chip"
data-sort="@localSort"
@onclick="() => SetSort(localSort)">
@SortLabel(localSort)
</button>
}
</div>
<div class="m-list-toolbar__row">
<div class="m-list-toolbar__group">
<label class="m-list-toolbar__label">@L["Anomaly.Filter.From"]</label>
@@ -198,6 +215,9 @@
private static readonly AnomalyKind[] _kindOptions =
{ AnomalyKind.SuspensionFlip, AnomalyKind.SteamMove, AnomalyKind.SuspensionFreeze, AnomalyKind.OverroundCompression };
private static readonly AnomalySort[] _sortOptions =
{ AnomalySort.Newest, AnomalySort.HighestScore, AnomalySort.LongestGap };
private List<AnomalyListItem> _items = new();
private IReadOnlyList<int> _availableSports = Array.Empty<int>();
private bool _loading = true;
@@ -287,6 +307,16 @@
_ => kind.ToString(),
};
private Task SetSort(AnomalySort sort) => UpdateFilter(_filter with { Sort = sort });
private string SortLabel(AnomalySort sort) => sort switch
{
AnomalySort.Newest => L["Anomaly.Sort.Newest"],
AnomalySort.HighestScore => L["Anomaly.Sort.Score"],
AnomalySort.LongestGap => L["Anomaly.Sort.Gap"],
_ => sort.ToString(),
};
private async Task OnFromChanged(ChangeEventArgs e)
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))