feat(web): update public session detail with showcase fields

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 16:13:35 +03:00
parent 71ffcce06b
commit 6d59737d07
3 changed files with 83 additions and 9 deletions
@@ -16,4 +16,5 @@ public sealed record ShowcaseSessionDto(
int? MaxPlayers, int? MaxPlayers,
int ActivePlayerCount, int ActivePlayerCount,
int WaitlistedPlayerCount, int WaitlistedPlayerCount,
bool AllowDirectRegistration); bool AllowDirectRegistration,
string? Description);
@@ -2,6 +2,7 @@
@layout PublicLayout @layout PublicLayout
@inject ISessionStore SessionStore @inject ISessionStore SessionStore
@inject NavigationManager Navigation @inject NavigationManager Navigation
@using GmRelay.Shared.Features.Showcase
<PageTitle>@PageTitleText</PageTitle> <PageTitle>@PageTitleText</PageTitle>
@@ -30,10 +31,29 @@ else if (session is not null)
<meta name="description" content="@($"Публичная сессия {session.Title} клуба {session.GroupName} в GM-Relay.")" /> <meta name="description" content="@($"Публичная сессия {session.Title} клуба {session.GroupName} в GM-Relay.")" />
</HeadContent> </HeadContent>
@if (!string.IsNullOrWhiteSpace(session.CoverImageUrl))
{
<div class="session-cover-hero" style="background-image: url('@session.CoverImageUrl')"></div>
}
<section class="public-hero public-hero-compact"> <section class="public-hero public-hero-compact">
<span class="status-badge @GetStatusClass(session.Status)">@TranslateStatus(session.Status)</span> <span class="status-badge @GetStatusClass(session.Status)">@TranslateStatus(session.Status)</span>
<h1>@session.Title</h1> <h1>@session.Title</h1>
<p>@session.GroupName</p> <p>@session.GroupName</p>
<div class="session-badges">
@if (!string.IsNullOrWhiteSpace(session.System))
{
<span class="status-badge status-info">@GetSystemDisplayName(session.System)</span>
}
@if (session.IsOneShot)
{
<span class="status-badge status-warning">Ваншот</span>
}
@if (!string.IsNullOrWhiteSpace(session.Format))
{
<span class="status-badge status-neutral">@TranslateFormat(session.Format)</span>
}
</div>
</section> </section>
<article class="glass-card public-session-detail"> <article class="glass-card public-session-detail">
@@ -50,14 +70,33 @@ else if (session is not null)
<span>Статус</span> <span>Статус</span>
<strong>@TranslateStatus(session.Status)</strong> <strong>@TranslateStatus(session.Status)</strong>
</div> </div>
@if (session.DurationMinutes.HasValue)
{
<div>
<span>Длительность</span>
<strong>@FormatDuration(session.DurationMinutes.Value)</strong>
</div>
}
</div> </div>
@if (!string.IsNullOrWhiteSpace(session.Description))
{
<div class="session-description">
<h3>Описание</h3>
<p>@session.Description</p>
</div>
}
<div class="public-settings-actions"> <div class="public-settings-actions">
@if (!string.IsNullOrWhiteSpace(session.GroupSlug)) @if (!string.IsNullOrWhiteSpace(session.GroupSlug))
{ {
<a class="btn-gm btn-gm-primary" href="@($"/club/{session.GroupSlug}")">Расписание клуба</a> <a class="btn-gm btn-gm-primary" href="@($"/club/{session.GroupSlug}")">Расписание клуба</a>
} }
<a class="btn-gm btn-gm-outline" href="@PublicSessionUrl" target="_blank" rel="noopener noreferrer">Ссылка на сессию</a> <a class="btn-gm btn-gm-outline" href="@PublicSessionUrl" target="_blank" rel="noopener noreferrer">Ссылка на сессию</a>
@if (session.AllowDirectRegistration)
{
<a class="btn-gm btn-gm-primary" href="@($"/s/{SessionId}?register=1")">Записаться</a>
}
</div> </div>
</article> </article>
} }
@@ -65,7 +104,7 @@ else if (session is not null)
@code { @code {
[Parameter] public Guid SessionId { get; set; } [Parameter] public Guid SessionId { get; set; }
private WebPublicSession? session; private ShowcaseSessionDto? session;
private bool loaded; private bool loaded;
private string PageTitleText => session is null ? "Публичная сессия — GM-Relay" : $"{session.Title} — GM-Relay"; private string PageTitleText => session is null ? "Публичная сессия — GM-Relay" : $"{session.Title} — GM-Relay";
@@ -75,11 +114,11 @@ else if (session is not null)
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
loaded = false; loaded = false;
session = await SessionStore.GetPublicSessionAsync(SessionId); session = await SessionStore.GetShowcaseSessionAsync(SessionId);
loaded = true; loaded = true;
} }
private static string FormatSeats(WebPublicSession session) private static string FormatSeats(ShowcaseSessionDto session)
{ {
var seats = session.MaxPlayers.HasValue var seats = session.MaxPlayers.HasValue
? $"{session.ActivePlayerCount}/{session.MaxPlayers.Value}" ? $"{session.ActivePlayerCount}/{session.MaxPlayers.Value}"
@@ -90,6 +129,35 @@ else if (session is not null)
: seats; : 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 GetSystemDisplayName(string? system)
{
if (string.IsNullOrWhiteSpace(system))
return system ?? string.Empty;
if (Enum.TryParse<GameSystem>(system, out var gs))
return gs.ToDisplayName();
return system;
}
private static string TranslateFormat(string format) => format switch
{
"Online" => "Онлайн",
"Offline" => "Офлайн",
"Hybrid" => "Гибрид",
_ => format
};
private static string GetStatusClass(string status) => status switch private static string GetStatusClass(string status) => status switch
{ {
SessionStatus.Confirmed => "status-success", SessionStatus.Confirmed => "status-success",
+10 -5
View File
@@ -127,7 +127,8 @@ internal sealed record ShowcaseSessionRow(
int? MaxPlayers, int? MaxPlayers,
int ActivePlayerCount, int ActivePlayerCount,
int WaitlistedPlayerCount, int WaitlistedPlayerCount,
bool AllowDirectRegistration); bool AllowDirectRegistration,
string? Description);
public sealed class SessionService( public sealed class SessionService(
NpgsqlDataSource dataSource, NpgsqlDataSource dataSource,
@@ -400,7 +401,8 @@ public sealed class SessionService(
s.max_players AS MaxPlayers, s.max_players AS MaxPlayers,
COALESCE(active_counts.count, 0)::int AS ActivePlayerCount, COALESCE(active_counts.count, 0)::int AS ActivePlayerCount,
COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount, COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount,
s.allow_direct_registration AS AllowDirectRegistration s.allow_direct_registration AS AllowDirectRegistration,
s.description AS Description
FROM sessions s FROM sessions s
JOIN game_groups g ON g.id = s.group_id JOIN game_groups g ON g.id = s.group_id
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
@@ -463,7 +465,8 @@ public sealed class SessionService(
return rows.Select(r => new ShowcaseSessionDto( return rows.Select(r => new ShowcaseSessionDto(
r.Id, r.GroupId, r.GroupName, r.GroupSlug, r.Title, r.ScheduledAt, r.Status, r.Id, r.GroupId, r.GroupName, r.GroupSlug, r.Title, r.ScheduledAt, r.Status,
r.System, r.IsOneShot, r.Format, r.DurationMinutes, r.CoverImageUrl, r.System, r.IsOneShot, r.Format, r.DurationMinutes, r.CoverImageUrl,
r.MaxPlayers, r.ActivePlayerCount, r.WaitlistedPlayerCount, r.AllowDirectRegistration)).ToList(); r.MaxPlayers, r.ActivePlayerCount, r.WaitlistedPlayerCount, r.AllowDirectRegistration,
r.Description)).ToList();
} }
public async Task<ShowcaseSessionDto?> GetShowcaseSessionAsync(Guid sessionId) public async Task<ShowcaseSessionDto?> GetShowcaseSessionAsync(Guid sessionId)
@@ -486,7 +489,8 @@ public sealed class SessionService(
s.max_players AS MaxPlayers, s.max_players AS MaxPlayers,
COALESCE(active_counts.count, 0)::int AS ActivePlayerCount, COALESCE(active_counts.count, 0)::int AS ActivePlayerCount,
COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount, COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount,
s.allow_direct_registration AS AllowDirectRegistration s.allow_direct_registration AS AllowDirectRegistration,
s.description AS Description
FROM sessions s FROM sessions s
JOIN game_groups g ON g.id = s.group_id JOIN game_groups g ON g.id = s.group_id
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
@@ -531,7 +535,8 @@ public sealed class SessionService(
return new ShowcaseSessionDto( return new ShowcaseSessionDto(
row.Id, row.GroupId, row.GroupName, row.GroupSlug, row.Title, row.ScheduledAt, row.Status, row.Id, row.GroupId, row.GroupName, row.GroupSlug, row.Title, row.ScheduledAt, row.Status,
row.System, row.IsOneShot, row.Format, row.DurationMinutes, row.CoverImageUrl, row.System, row.IsOneShot, row.Format, row.DurationMinutes, row.CoverImageUrl,
row.MaxPlayers, row.ActivePlayerCount, row.WaitlistedPlayerCount, row.AllowDirectRegistration); row.MaxPlayers, row.ActivePlayerCount, row.WaitlistedPlayerCount, row.AllowDirectRegistration,
row.Description);
} }
public async Task<bool> RegisterFromShowcaseAsync(Guid sessionId, string platform, string externalUserId, string displayName) public async Task<bool> RegisterFromShowcaseAsync(Guid sessionId, string platform, string externalUserId, string displayName)