From 72f43dbef2b03d14290fc92da11a79f4aa189bb0 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Thu, 28 May 2026 16:00:21 +0300 Subject: [PATCH] feat(web): add /showcase catalog page with filters Co-Authored-By: Claude Opus 4.7 --- .../Components/Pages/Showcase.razor | 423 ++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 src/GmRelay.Web/Components/Pages/Showcase.razor diff --git a/src/GmRelay.Web/Components/Pages/Showcase.razor b/src/GmRelay.Web/Components/Pages/Showcase.razor new file mode 100644 index 0000000..7acdddc --- /dev/null +++ b/src/GmRelay.Web/Components/Pages/Showcase.razor @@ -0,0 +1,423 @@ +@page "/showcase" +@layout PublicLayout +@inject ISessionStore SessionStore +@inject NavigationManager Navigation +@using GmRelay.Shared.Features.Showcase + +Каталог игр — GM-Relay + + + + + +
+

Каталог игр

+

Найдите настольную ролевую игру по душе — ваншоты, кампании, онлайн и офлайн.

+
+ +
+
+ Когда +
+ + + + +
+
+ +
+ Места +
+ + + +
+
+ +
+ Система + +
+ +
+ Тип +
+ + + +
+
+ +
+ Формат +
+ + + + +
+
+
+ +@if (loading && sessions.Count == 0) +{ +
+ @for (var i = 0; i < 6; i++) + { +
+
+
+
+
+
+
+
+ } +
+} +else if (!loading && sessions.Count == 0) +{ +
+

Игры не найдены

+

Попробуйте изменить фильтры или загляните позже — новые сессии появляются каждый день.

+
+} +else +{ +
+ @foreach (var session in sessions) + { +
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(session.System)) + { + @GetSystemDisplayName(session.System) + } + @if (session.IsOneShot) + { + Ваншот + } + @if (!string.IsNullOrWhiteSpace(session.Format)) + { + @TranslateFormat(session.Format) + } +
+

@session.Title

+
+ @session.ScheduledAt.FormatMoscow() + @if (session.DurationMinutes.HasValue) + { + @FormatDuration(session.DurationMinutes.Value) + } +
+
+ @FormatSeats(session) +
+
+ @session.GroupName +
+
+ Подробнее + @if (session.AllowDirectRegistration) + { + + } +
+
+
+ } +
+ + @if (hasMore) + { +
+ +
+ } +} + + + +@code { + private ShowcaseFilter filter = new(); + private List sessions = new(); + private bool loading; + private bool hasMore; + private int page = 1; + private const int PageSize = 12; + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + private async Task LoadAsync() + { + loading = true; + page = 1; + sessions.Clear(); + var results = await SessionStore.GetShowcaseSessionsAsync(filter, page, PageSize); + sessions.AddRange(results); + hasMore = results.Count == PageSize; + loading = false; + } + + private async Task LoadMoreAsync() + { + loading = true; + page++; + var results = await SessionStore.GetShowcaseSessionsAsync(filter, page, PageSize); + sessions.AddRange(results); + hasMore = results.Count == PageSize; + loading = false; + } + + private async Task OnFilterChanged() + { + await LoadAsync(); + } + + private async Task SetDate(DateFilter value) + { + filter = filter with { Date = value }; + await OnFilterChanged(); + } + + private async Task SetSeats(SeatFilter value) + { + filter = filter with { Seats = value }; + await OnFilterChanged(); + } + + private async Task OnSystemChanged(ChangeEventArgs e) + { + var value = e.Value?.ToString(); + filter = filter with { System = string.IsNullOrWhiteSpace(value) ? null : value }; + await OnFilterChanged(); + } + + private async Task SetOneShot(bool? value) + { + filter = filter with { IsOneShot = value }; + await OnFilterChanged(); + } + + private async Task SetFormat(string? value) + { + filter = filter with { Format = value }; + await OnFilterChanged(); + } + + private static string GetGradientStyle(Guid id) + { + var bytes = id.ToByteArray(); + var hue1 = bytes[0] % 360; + var hue2 = (bytes[1] + 120) % 360; + return $"linear-gradient(135deg, hsl({hue1}, 55%, 28%) 0%, hsl({hue2}, 55%, 20%) 100%)"; + } + + private static string GetSystemDisplayName(string? system) + { + if (string.IsNullOrWhiteSpace(system)) + return system ?? string.Empty; + + if (Enum.TryParse(system, out var gs)) + return gs.ToDisplayName(); + + return system; + } + + private static string FormatSeats(ShowcaseSessionDto session) + { + var seats = session.MaxPlayers.HasValue + ? $"{session.ActivePlayerCount}/{session.MaxPlayers.Value}" + : $"{session.ActivePlayerCount} игроков"; + + if (session.WaitlistedPlayerCount > 0) + seats += $", ожидание {session.WaitlistedPlayerCount}"; + + return seats; + } + + private static string FormatDuration(int minutes) + { + if (minutes < 60) + return $"{minutes} мин"; + + var hours = minutes / 60; + var mins = minutes % 60; + return mins > 0 ? $"{hours} ч {mins} мин" : $"{hours} ч"; + } + + private static string TranslateFormat(string format) => format switch + { + "Online" => "Онлайн", + "Offline" => "Офлайн", + "Hybrid" => "Гибрид", + _ => format + }; +}