diff --git a/src/GmRelay.DiscordBot/Rendering/DiscordSessionBatchRenderer.cs b/src/GmRelay.DiscordBot/Rendering/DiscordSessionBatchRenderer.cs new file mode 100644 index 0000000..81aecf0 --- /dev/null +++ b/src/GmRelay.DiscordBot/Rendering/DiscordSessionBatchRenderer.cs @@ -0,0 +1,125 @@ +using GmRelay.Shared.Domain; +using GmRelay.Shared.Rendering; +using NetCord; +using NetCord.Rest; + +namespace GmRelay.DiscordBot.Rendering; + +public static class DiscordSessionBatchRenderer +{ + public static (IReadOnlyList Embeds, IReadOnlyList ActionRows) Render(SessionBatchViewModel view) + { + var embeds = new List(); + var actionRows = new List(); + + foreach (var session in view.Sessions) + { + var embed = BuildEmbed(view.Title, session); + embeds.Add(embed); + + if (session.AvailableActions.Count > 0) + { + var actionRow = new ActionRowProperties(); + foreach (var action in session.AvailableActions) + { + actionRow.Add(new ButtonProperties( + $"{action.ActionKey}:{action.SessionId}", + action.Label, + ButtonStyle.Primary)); + } + actionRows.Add(actionRow); + } + } + + return (embeds, actionRows); + } + + private static EmbedProperties BuildEmbed(string title, SessionViewItem session) + { + var embed = new EmbedProperties() + .WithTitle($"{title} — {session.ScheduledAt.FormatMoscow()}"); + + if (SessionStatus.IsCancelled(session.Status)) + { + embed = embed.WithDescription("❌ Сессия отменена"); + } + else + { + embed = embed.WithDescription(BuildPlayerDescription(session)); + } + + var fields = new List + { + new EmbedFieldProperties() + .WithName("👥 Заполненность") + .WithValue(session.MaxPlayers.HasValue + ? $"{session.ActivePlayerCount}/{session.MaxPlayers.Value}" + : $"{session.ActivePlayerCount}") + .WithInline(), + + new EmbedFieldProperties() + .WithName("⏳ Лист ожидания") + .WithValue(session.WaitlistedPlayers.Count > 0 + ? session.WaitlistedPlayers.Count.ToString() + : "—") + .WithInline(), + + new EmbedFieldProperties() + .WithName("📊 Статус") + .WithValue(FormatStatus(session.Status)) + .WithInline() + }; + + if (!string.IsNullOrEmpty(session.JoinLink)) + { + embed = embed.WithUrl(session.JoinLink); + } + + embed = embed.WithColor(GetColor(session)); + embed = embed.AddFields(fields); + + return embed; + } + + private static string BuildPlayerDescription(SessionViewItem session) + { + if (session.ActivePlayers.Count == 0) + return "👥 Пока никто не записался"; + + var lines = session.ActivePlayers + .Select(p => $"• {p.DisplayName}") + .ToList(); + + if (session.WaitlistedPlayers.Count > 0) + { + lines.Add(""); + lines.Add($"⏳ Лист ожидания ({session.WaitlistedPlayers.Count}):"); + lines.AddRange(session.WaitlistedPlayers.Select(p => $"• {p.DisplayName}")); + } + + return string.Join('\n', lines); + } + + private static string FormatStatus(string status) => status switch + { + SessionStatus.Planned => "Запланирована", + SessionStatus.ConfirmationSent => "Ожидает подтверждения", + SessionStatus.Confirmed => "Подтверждена", + SessionStatus.Cancelled => "Отменена", + _ => status + }; + + private static Color GetColor(SessionViewItem session) + { + if (SessionStatus.IsCancelled(session.Status)) + return new Color(0xED4245); + + if (session.Status == SessionStatus.Confirmed) + return new Color(0x5865F2); + + if (session.MaxPlayers.HasValue && session.ActivePlayerCount >= session.MaxPlayers.Value) + return new Color(0xFEE75C); + + return new Color(0x57F287); + } +} diff --git a/src/GmRelay.Shared/Rendering/DiscordSessionBatchRenderer.cs b/src/GmRelay.Shared/Rendering/DiscordSessionBatchRenderer.cs deleted file mode 100644 index a796f16..0000000 --- a/src/GmRelay.Shared/Rendering/DiscordSessionBatchRenderer.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace GmRelay.Shared.Rendering; - -/// -/// Заглушка для Discord-рендерера. -/// Реальная реализация будет добавлена в проект GmRelay.DiscordBot (issue #26). -/// -public static class DiscordSessionBatchRenderer -{ - public static object Render(SessionBatchViewModel view) - { - throw new NotImplementedException("Discord renderer will be implemented in issue #26."); - } -} diff --git a/tests/GmRelay.Bot.Tests/Rendering/DiscordSessionBatchRendererTests.cs b/tests/GmRelay.Bot.Tests/Rendering/DiscordSessionBatchRendererTests.cs index febf077..b8be82e 100644 --- a/tests/GmRelay.Bot.Tests/Rendering/DiscordSessionBatchRendererTests.cs +++ b/tests/GmRelay.Bot.Tests/Rendering/DiscordSessionBatchRendererTests.cs @@ -35,17 +35,17 @@ public sealed class DiscordSessionBatchRendererTests Assert.Equal(2, actionRows.Count); // cancelled skipped // Embed titles contain game title and Moscow date - Assert.Contains(embeds, e => e.Title.Contains("Campaign") && e.Title.Contains("26")); - Assert.Contains(embeds, e => e.Title.Contains("Campaign") && e.Title.Contains("27")); - Assert.Contains(embeds, e => e.Title.Contains("Campaign") && e.Title.Contains("28")); + Assert.Contains(embeds, e => e.Title!.Contains("Campaign") && e.Title.Contains("26")); + Assert.Contains(embeds, e => e.Title!.Contains("Campaign") && e.Title.Contains("27")); + Assert.Contains(embeds, e => e.Title!.Contains("Campaign") && e.Title.Contains("28")); // Cancelled session embed description indicates cancellation - var cancelledEmbed = embeds.First(e => e.Description.Contains("отменена") || e.Description.Contains("Отменена")); + var cancelledEmbed = embeds.First(e => e.Description!.Contains("отменена") || e.Description.Contains("Отменена")); Assert.NotNull(cancelledEmbed); // Active session embeds contain player names - Assert.Contains(embeds, e => e.Description != null && e.Description.Contains("Alice")); - Assert.Contains(embeds, e => e.Description != null && e.Description.Contains("Charlie")); + Assert.Contains(embeds, e => e.Description!.Contains("Alice")); + Assert.Contains(embeds, e => e.Description!.Contains("Charlie")); // Buttons for active sessions var allButtons = actionRows.SelectMany(r => r).OfType().ToList(); @@ -94,7 +94,7 @@ public sealed class DiscordSessionBatchRendererTests var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); - Assert.Equal(0xED4245, embeds[0].Color!.Value.RawValue); + Assert.Equal(0xED4245, embeds[0].Color.RawValue); } [Fact] @@ -107,7 +107,7 @@ public sealed class DiscordSessionBatchRendererTests var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); - Assert.Equal(0x57F287, embeds[0].Color!.Value.RawValue); + Assert.Equal(0x57F287, embeds[0].Color.RawValue); } [Fact] @@ -120,7 +120,7 @@ public sealed class DiscordSessionBatchRendererTests var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); - Assert.Equal(0xFEE75C, embeds[0].Color!.Value.RawValue); + Assert.Equal(0xFEE75C, embeds[0].Color.RawValue); } [Fact]