7.8 KiB
Game Catalog and One-Shot Showcase — Design Spec
Issue #39: feat: добавить каталог игр и витрину ваншотов
Goal
Build a public /showcase page that aggregates published sessions from all clubs into a filterable catalog. Users can browse games by system, format, date, and availability. GM controls whether direct registration from the catalog is allowed. The catalog respects existing seat limits and waitlist logic.
Architecture
Extend the existing public-pages infrastructure (V026) with new session metadata fields, a cross-group query layer in ISessionStore, and new Razor pages in GmRelay.Web. Bot flows (Telegram + Discord) are updated to collect the new fields during session creation. Fuzzy matching on game system names is performed client-side in the bot UI.
Tech Stack
- .NET 10, Blazor Server, Dapper.AOT, Npgsql
- Existing:
PublicLayout,ISessionStore,SessionService,SessionCapacityRules - New:
GameSystemenum,ShowcaseFilterrecord,ShowcaseSessionDto
Data Model
New Fields on sessions (Migration V027)
| Column | Type | Constraints | Description |
|---|---|---|---|
is_one_shot |
BOOLEAN |
NOT NULL DEFAULT false |
One-shot or campaign |
system |
VARCHAR(50) |
nullable | Game system name (enum value or custom) |
description |
TEXT |
nullable | Short description for card |
cover_image_url |
TEXT |
nullable | Cover image URL |
duration_minutes |
INTEGER |
nullable | Duration in minutes |
format |
VARCHAR(20) |
CHECK (format IN ('Online','Offline','Hybrid')), nullable |
Session format |
allow_direct_registration |
BOOLEAN |
NOT NULL DEFAULT false |
Allow direct registration from showcase |
GameSystem Enum
public enum GameSystem
{
Dnd5e, Pathfinder2e, CallOfCthulhu7e, Shadowdark,
OldSchoolEssentials, Dragonbane, BladesInTheDark,
Daggerheart, CyberpunkRed, Mothership, AlienRpg,
WarhammerFantasy, VampireMasquerade5e, StarWarsFfg,
Genesys, SavageWorlds, GURPS, Fate, DungeonWorld,
Ironsworn, Other
}
Stored as VARCHAR(50) in DB (not native enum) to allow future extension without migration.
DTOs
public sealed record ShowcaseSessionDto(
Guid Id,
Guid GroupId,
string GroupName,
string? GroupSlug,
string Title,
DateTime ScheduledAt,
string Status,
string? System,
bool IsOneShot,
string? Format,
int? DurationMinutes,
string? CoverImageUrl,
int? MaxPlayers,
int ActivePlayerCount,
int WaitlistedPlayerCount,
bool AllowDirectRegistration);
public sealed record ShowcaseFilter(
DateFilter Date = DateFilter.All,
SeatFilter Seats = SeatFilter.Any,
GameSystem? System = null,
bool? IsOneShot = null,
string? Format = null);
public enum DateFilter { Today, Tomorrow, ThisWeek, All }
public enum SeatFilter { Available, Waitlist, Any }
UI Design
/showcase — Catalog Page
Layout:
- Hero with title "Каталог игр"
- Sticky filter bar (horizontal on desktop, collapsible on mobile)
- Responsive grid of session cards (1 col mobile, 2 col tablet, 3 col desktop)
- Pagination (page + pageSize = 12)
Filters:
- Date: "Сегодня" | "Завтра" | "На неделю" | "Все"
- Seats: "Есть места" | "Waitlist" | "Любое"
- System: dropdown with all
GameSystemvalues - Type: "Ваншот" | "Кампания" | "Любое"
- Format: "Онлайн" | "Офлайн" | "Гибрид" | "Любое"
Card Design:
- Cover image (fallback: colored placeholder with initials)
- Title
- System badge
- Date + time (MSK)
- Duration (e.g. "3 часа")
- Format badge
- Seats indicator: "5/6 мест" | "Waitlist (3)" | "Мест нет"
- Club name (link to
/club/{slug}) - Buttons: "Подробнее" →
/s/{id}, "Записаться" (ifAllowDirectRegistration)
/s/{id} — Public Session Detail (Updated)
New fields added to existing page:
- Cover image (full-width hero)
- System badge
- Description block
- Duration + format
- GM contact (always visible: Telegram username or Discord tag)
- If
allow_direct_registration:- "Записаться" button → Telegram Mini App deeplink or Discord OAuth
- Direct registration into
session_participantsviaSessionCapacityRules
Backend
ISessionStore Methods
Task<IReadOnlyList<ShowcaseSessionDto>> GetShowcaseSessionsAsync(
ShowcaseFilter filter, int page, int pageSize);
Task<ShowcaseSessionDto?> GetShowcaseSessionAsync(Guid sessionId);
Task<bool> RegisterFromShowcaseAsync(Guid sessionId, PlatformUser user);
GetShowcaseSessionsAsync query:
- Cross-group (all clubs with
public_schedule_enabled = true) - Only
is_public = truesessions scheduled_at > now() - interval '4 hours'status <> 'Cancelled'- Apply filters in SQL WHERE clause
- Order by
scheduled_at ASC - Offset/limit pagination
RegisterFromShowcaseAsync:
- Check
allow_direct_registration = true - Load session with
FOR UPDATE - Count active + waitlisted participants
- Use
SessionCapacityRules.DecideJoinStatus - Insert participant with appropriate
registration_status - Return true on success, false if full and no waitlist allowed
Bot Integration
Telegram Bot
During CreateSessionCommand flow, after title/link/time input:
- "Выберите систему:" inline keyboard with
GameSystemvalues + "Другое" - If text input instead of button: fuzzy match against display names (Levenshtein/Contains/StartsWith)
- "Описание игры (краткое):" — text input, optional (skip button)
- "Формат:" inline keyboard — "Онлайн" | "Офлайн" | "Гибрид"
- "Продолжительность (в часах):" — int input, optional
- "Обложка (URL или пропустить):" — text input, optional
During /publish flow:
- "Разрешить прямую запись из каталога?" — yes/no toggle (default: no)
Discord Bot
Same flow adapted for Discord interactions:
- Slash command options or button menus for system/format
- Modal input for description, duration, cover URL
- Fuzzy matching on free-text system input
Migration V027
ALTER TABLE sessions
ADD COLUMN is_one_shot BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN system VARCHAR(50),
ADD COLUMN description TEXT,
ADD COLUMN cover_image_url TEXT,
ADD COLUMN duration_minutes INTEGER,
ADD COLUMN format VARCHAR(20) CHECK (format IN ('Online','Offline','Hybrid')),
ADD COLUMN allow_direct_registration BOOLEAN NOT NULL DEFAULT false;
CREATE INDEX ix_sessions_showcase
ON sessions (scheduled_at, system, is_one_shot, format)
WHERE is_public = true AND status <> 'Cancelled';
Testing Strategy
- Unit tests:
SessionCapacityRuleswith showcase registration scenarios - Integration tests:
GetShowcaseSessionsAsyncwith each filter combination - UI tests:
Showcase.razorrendering with/without cover images, filters applied - Bot tests: Fuzzy matching algorithm for
GameSystemresolution
Version Bump
Issue label: type:feature → minor bump
Current: 3.3.0 → Next: 3.4.0
Files to sync:
Directory.Build.propscompose.yaml(bot, discord, web image tags).gitea/workflows/deploy.yml(VERSIONenv)src/GmRelay.Web/Components/Layout/NavMenu.razor
Acceptance Criteria (from Issue #39)
- User can find a published game without accessing a private dashboard
- Registration does not bypass existing seat/waitlist limits
- Owner/co-GM controls what appears in the showcase via
is_public+allow_direct_registration - Filters work: date, seats, system, type, format
- GM contact is always visible on public session detail
- Direct registration respects
SessionCapacityRules