From be86a2a08afa49778a0919d703db1ac0fcda5034 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Thu, 4 Jun 2026 08:33:53 +0300 Subject: [PATCH] feat(wizard): add WizardStep renderer (single + pool steps) --- .../CreateSession/Wizard/WizardStep.cs | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStep.cs diff --git a/src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStep.cs b/src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStep.cs new file mode 100644 index 0000000..df95f30 --- /dev/null +++ b/src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStep.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Text; +using GmRelay.Shared.Domain; +using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; +using Telegram.Bot.Types.ReplyMarkups; + +namespace GmRelay.Bot.Features.Sessions.CreateSession.Wizard; + +public static class WizardStep +{ + public const int MaxTitleLength = 200; + public const int MaxDescriptionLength = 4000; + public const int MaxSystemLength = 100; + public const int MaxCapacity = 50; + public const int MinCapacity = 1; + public const int MinDurationHours = 1; + public const int MaxDurationHours = 12; + + public static (string text, InlineKeyboardMarkup keyboard) Render( + WizardDraft draft, + WizardPayload payload, + IReadOnlyList? clubs = null) + { + return draft.Step switch + { + WizardStepNames.Type => RenderType(), + WizardStepNames.Title => RenderTitle(), + WizardStepNames.Description => RenderDescription(), + WizardStepNames.Cover => RenderCover(), + WizardStepNames.System => RenderSystem(), + WizardStepNames.Duration => RenderDuration(), + WizardStepNames.DateTime => RenderDateTime(), + WizardStepNames.Capacity => RenderCapacity(), + WizardStepNames.Visibility => RenderVisibility(), + WizardStepNames.PickClub => RenderPickClub(clubs ?? Array.Empty()), + WizardStepNames.Publish => RenderPublish(), + WizardStepNames.Confirm => RenderSingleConfirm(payload), + + WizardStepNames.PoolSystemDuration => RenderPoolSystemDuration(), + WizardStepNames.PoolAddSlots => RenderPoolAddSlots(payload), + WizardStepNames.PoolSlotDateTime => RenderPoolSlotDateTime(), + WizardStepNames.PoolSlotCapacity => RenderPoolSlotCapacity(), + WizardStepNames.PoolConfirm => RenderPoolConfirm(payload), + + _ => throw new InvalidOperationException($"Unknown wizard step: {draft.Step}"), + }; + } + + // ── Single-game renderers ────────────────────────────────────────── + private static (string, InlineKeyboardMarkup) RenderType() => ( + "🎲 Создание новой игровой сессии\n\nЧто создаём?", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("🎯 Одну игру", WizardCallbackData.Choice(WizardStepNames.Type, "single")) }, + new[] { InlineKeyboardButton.WithCallbackData("📅 Пул игр", WizardCallbackData.Choice(WizardStepNames.Type, "pool")) }, + new[] { InlineKeyboardButton.WithCallbackData("❌ Отмена", WizardCallbackData.Cancel()) }, + })); + + private static (string, InlineKeyboardMarkup) RenderTitle() => ( + "📝 Введите название игры одним сообщением.", + BackCancel()); + + private static (string, InlineKeyboardMarkup) RenderDescription() => ( + "📄 Введите описание (или «-», чтобы пропустить).", + SkipBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderCover() => ( + "🖼 Пришлите картинку как вложение или URL (или «-»).", + SkipBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderSystem() + { + var buttons = new List + { + new[] { InlineKeyboardButton.WithCallbackData("D&D 5e", WizardCallbackData.Choice(WizardStepNames.System, "Dnd5e")) }, + new[] { InlineKeyboardButton.WithCallbackData("Pathfinder 2e", WizardCallbackData.Choice(WizardStepNames.System, "Pathfinder2e")) }, + new[] { InlineKeyboardButton.WithCallbackData("Call of Cthulhu",WizardCallbackData.Choice(WizardStepNames.System, "CallOfCthulhu7e")) }, + new[] { InlineKeyboardButton.WithCallbackData("GURPS", WizardCallbackData.Choice(WizardStepNames.System, "GURPS")) }, + new[] { InlineKeyboardButton.WithCallbackData("Fate", WizardCallbackData.Choice(WizardStepNames.System, "Fate")) }, + new[] { InlineKeyboardButton.WithCallbackData("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.System, "_other")) }, + new[] { InlineKeyboardButton.WithCallbackData("⏭ Пропустить", WizardCallbackData.Choice(WizardStepNames.System, "_skip")) }, + }; + return ("🎲 Выберите систему.", new InlineKeyboardMarkup(buttons).AppendBackCancel()); + } + + private static (string, InlineKeyboardMarkup) RenderDuration() => ( + "⏱ Выберите длительность.", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("3 часа", WizardCallbackData.Choice(WizardStepNames.Duration, "180")) }, + new[] { InlineKeyboardButton.WithCallbackData("4 часа", WizardCallbackData.Choice(WizardStepNames.Duration, "240")) }, + new[] { InlineKeyboardButton.WithCallbackData("5 часов", WizardCallbackData.Choice(WizardStepNames.Duration, "300")) }, + new[] { InlineKeyboardButton.WithCallbackData("6 часов", WizardCallbackData.Choice(WizardStepNames.Duration, "360")) }, + new[] { InlineKeyboardButton.WithCallbackData("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.Duration, "_other")) }, + new[] { InlineKeyboardButton.WithCallbackData("⏭ Пропустить", WizardCallbackData.Choice(WizardStepNames.Duration, "_skip")) }, + }).AppendBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderDateTime() => ( + "📅 Введите дату и время в формате ДД.ММ.ГГГГ ЧЧ:ММ (Москва).", + BackCancel()); + + private static (string, InlineKeyboardMarkup) RenderCapacity() => ( + "👥 Введите лимит мест (1..50) одним числом.\nЗатем нажмите кнопку waitlist.", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("✅ Waitlist вкл", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on")) }, + new[] { InlineKeyboardButton.WithCallbackData("❌ Без waitlist", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:off")) }, + }).AppendBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderVisibility() => ( + "🔒 Выберите видимость.", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("🌐 Публичная в общем showcase", WizardCallbackData.Choice(WizardStepNames.Visibility, "public")) }, + new[] { InlineKeyboardButton.WithCallbackData("🏠 Публичная в витрине клуба", WizardCallbackData.Choice(WizardStepNames.Visibility, "club")) }, + new[] { InlineKeyboardButton.WithCallbackData("🔐 Только для членов клуба", WizardCallbackData.Choice(WizardStepNames.Visibility, "members")) }, + new[] { InlineKeyboardButton.WithCallbackData("🏷 Выбрать клуб…", WizardCallbackData.Choice(WizardStepNames.Visibility, "pickclub")) }, + }).AppendBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderPickClub(IReadOnlyList clubs) + { + if (clubs.Count == 0) + { + return ( + "🏷 У вас нет клубов. Создайте клуб в Web dashboard и вернитесь.", + BackCancel()); + } + var rows = new List(); + foreach (var club in clubs) + { + rows.Add(new[] + { + InlineKeyboardButton.WithCallbackData(club.Name, WizardCallbackData.Choice(WizardStepNames.PickClub, club.ClubId.ToString())) + }); + } + return ("🏷 Выберите клуб:", new InlineKeyboardMarkup(rows).AppendBackCancel()); + } + + private static (string, InlineKeyboardMarkup) RenderPublish() => ( + "✨ Опубликовать в витрине сейчас?", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("✅ Опубликовать", WizardCallbackData.Choice(WizardStepNames.Publish, "yes")) }, + new[] { InlineKeyboardButton.WithCallbackData("📝 Только в чате", WizardCallbackData.Choice(WizardStepNames.Publish, "no")) }, + }).AppendBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderSingleConfirm(WizardPayload p) + { + var sb = new StringBuilder(); + sb.AppendLine("👀 Проверьте перед созданием:"); + sb.AppendLine(); + sb.AppendLine($"🎲 {p.Title}"); + if (!string.IsNullOrEmpty(p.Description)) sb.AppendLine($"📄 {p.Description}"); + if (!string.IsNullOrEmpty(p.System)) sb.AppendLine($"🎲 Система: {p.System}"); + if (p.DurationMinutes.HasValue) sb.AppendLine($"⏱ Длительность: {p.DurationMinutes / 60} ч"); + if (p.Single?.ScheduledAt is { } at) sb.AppendLine($"📅 {at.FormatMoscow()} (МСК)"); + if (p.Single?.MaxPlayers is { } mp) sb.AppendLine($"👥 Мест: {mp}, waitlist {(p.Waitlist == true ? "вкл" : "выкл")}"); + sb.AppendLine($"🔒 Видимость: {RenderVisibilityText(p.Visibility)}"); + return (sb.ToString(), new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("✅ Создать", WizardCallbackData.Create()) }, + new[] { InlineKeyboardButton.WithCallbackData("⬅️ Назад", WizardCallbackData.Back()) }, + new[] { InlineKeyboardButton.WithCallbackData("❌ Отмена", WizardCallbackData.Cancel()) }, + })); + } + + // ── Pool renderers ───────────────────────────────────────────────── + private static (string, InlineKeyboardMarkup) RenderPoolSystemDuration() => ( + "🎲 Выберите систему и длительность пула.", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("D&D 5e · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Dnd5e:240")) }, + new[] { InlineKeyboardButton.WithCallbackData("Pathfinder 2e · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Pathfinder2e:240")) }, + new[] { InlineKeyboardButton.WithCallbackData("Call of Cthulhu · 3 ч",WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "CallOfCthulhu7e:180")) }, + new[] { InlineKeyboardButton.WithCallbackData("GURPS · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "GURPS:240")) }, + new[] { InlineKeyboardButton.WithCallbackData("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "_custom")) }, + }).AppendBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderPoolAddSlots(WizardPayload p) => ( + $"📅 Слоты пула «{p.Title}»\n\nДобавлено: {(p.Pool?.Slots.Count ?? 0)}", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("➕ Добавить слот", WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add")) }, + new[] { InlineKeyboardButton.WithCallbackData("✅ Готово, к превью", WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done")) }, + }).AppendBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderPoolSlotDateTime() => ( + "📅 Введите дату/время слота (ДД.ММ.ГГГГ ЧЧ:ММ).", + BackCancel()); + + private static (string, InlineKeyboardMarkup) RenderPoolSlotCapacity() => ( + "👥 Введите лимит мест (1..50) и выберите waitlist.", + new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("✅ Waitlist вкл", WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:on")) }, + new[] { InlineKeyboardButton.WithCallbackData("❌ Без waitlist", WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:off")) }, + }).AppendBackCancel()); + + private static (string, InlineKeyboardMarkup) RenderPoolConfirm(WizardPayload p) + { + var sb = new StringBuilder(); + sb.AppendLine("👀 Проверьте пул перед созданием:"); + sb.AppendLine(); + sb.AppendLine($"📝 {p.Title}"); + if (!string.IsNullOrEmpty(p.Description)) sb.AppendLine($"📄 {p.Description}"); + if (!string.IsNullOrEmpty(p.System)) sb.AppendLine($"🎲 Система: {p.System}"); + if (p.DurationMinutes.HasValue) sb.AppendLine($"⏱ Длительность: {p.DurationMinutes / 60} ч"); + sb.AppendLine($"🔒 Видимость: {RenderVisibilityText(p.Visibility)}"); + sb.AppendLine(); + sb.AppendLine($"Слоты ({p.Pool?.Slots.Count ?? 0}):"); + if (p.Pool is not null) + { + foreach (var s in p.Pool.Slots) + { + sb.AppendLine($" • {s.ScheduledAt.FormatMoscow()} — мест {s.MaxPlayers}, waitlist {(s.Waitlist ? "вкл" : "выкл")}"); + } + } + return (sb.ToString(), new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("✅ Создать пул", WizardCallbackData.Create()) }, + new[] { InlineKeyboardButton.WithCallbackData("⬅️ Назад", WizardCallbackData.Back()) }, + new[] { InlineKeyboardButton.WithCallbackData("❌ Отмена", WizardCallbackData.Cancel()) }, + })); + } + + // ── Helpers ──────────────────────────────────────────────────────── + private static InlineKeyboardMarkup BackCancel() => new(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("⬅️ Назад", WizardCallbackData.Back()) }, + new[] { InlineKeyboardButton.WithCallbackData("❌ Отмена", WizardCallbackData.Cancel()) }, + }); + + private static InlineKeyboardMarkup SkipBackCancel() => new(new[] + { + new[] { InlineKeyboardButton.WithCallbackData("⏭ Пропустить", WizardCallbackData.Choice("Skip", "1")) }, + new[] { InlineKeyboardButton.WithCallbackData("⬅️ Назад", WizardCallbackData.Back()) }, + new[] { InlineKeyboardButton.WithCallbackData("❌ Отмена", WizardCallbackData.Cancel()) }, + }); + + private static string RenderVisibilityText(WizardVisibility? v) => v switch + { + WizardVisibility.Public => "публичная в общем showcase", + WizardVisibility.Club => "публичная в витрине клуба", + WizardVisibility.Members => "только для членов клуба", + _ => "не задана", + }; +} + +internal static class InlineKeyboardMarkupExtensions +{ + public static InlineKeyboardMarkup AppendBackCancel(this InlineKeyboardMarkup kb) => kb; +}