diff --git a/.run/DevEnvironment.run.xml b/.run/DevEnvironment.run.xml index 713b9be..71551fb 100644 --- a/.run/DevEnvironment.run.xml +++ b/.run/DevEnvironment.run.xml @@ -7,10 +7,15 @@ - + - + @@ -19,8 +24,8 @@ - - + + diff --git a/Elternbeirat.Web.Tests/PocketBaseFixture.cs b/Elternbeirat.Web.Tests/PocketBaseFixture.cs index 7953818..d95ad8b 100644 --- a/Elternbeirat.Web.Tests/PocketBaseFixture.cs +++ b/Elternbeirat.Web.Tests/PocketBaseFixture.cs @@ -260,10 +260,12 @@ public sealed class PocketBaseFixture : IAsyncLifetime title = "Förderverein", body = "# Förderverein", location = "header", order = 3, slug = "patrons", embed = Array.Empty(), @public = true, }); + // Nav placeholder for the FAQ list (route /faqs is served by FaqList, + // which shadows this page). Slug matches production's "faqs" nav entry. await CreateRecordAsync(http, "pages", new { title = "FAQ", body = "# Häufige Fragen", location = "header", - order = 4, slug = "faq", embed = Array.Empty(), @public = true, + order = 4, slug = "faqs", embed = Array.Empty(), @public = true, }); await CreateRecordAsync(http, "pages", new { diff --git a/Elternbeirat.Web.Tests/RouteSmokeTests.cs b/Elternbeirat.Web.Tests/RouteSmokeTests.cs index 5f8a73d..a805f4e 100644 --- a/Elternbeirat.Web.Tests/RouteSmokeTests.cs +++ b/Elternbeirat.Web.Tests/RouteSmokeTests.cs @@ -48,7 +48,7 @@ public sealed class RouteSmokeTests : IDisposable "/", "/board", "/patrons", - "/faq", + "/faqs", "/faq-mensa", "/downloads", "/contact", @@ -83,4 +83,19 @@ public sealed class RouteSmokeTests : IDisposable response.StatusCode.ShouldBe(HttpStatusCode.NotFound); } + + [Fact] + public async Task Faqs_route_renders_the_grouped_list() + { + // /faqs is a literal route (FaqList) that must win over the /{Slug} + // catch-all content page. Assert it shows the FAQ list, grouped by topic + // from the seeded questions, not a content page body. + var client = _factory.CreateClient(); + + var html = await client.GetStringAsync(new Uri("/faqs", UriKind.Relative)); + + html.ShouldContain("Häufige Fragen"); + html.ShouldContain("Wann gibt es Mittagessen?"); + html.ShouldContain("Mensa und Mittagessen"); // German topic heading + } } diff --git a/Elternbeirat.Web/Components/Layout/MainLayout.razor b/Elternbeirat.Web/Components/Layout/MainLayout.razor index 56d549d..4d2d293 100644 --- a/Elternbeirat.Web/Components/Layout/MainLayout.razor +++ b/Elternbeirat.Web/Components/Layout/MainLayout.razor @@ -10,7 +10,7 @@ Förderverein Beiträge Termine - FAQ + FAQ Downloads Kontakt diff --git a/Elternbeirat.Web/Features/Faqs/FaqList.razor b/Elternbeirat.Web/Features/Faqs/FaqList.razor new file mode 100644 index 0000000..07053a7 --- /dev/null +++ b/Elternbeirat.Web/Features/Faqs/FaqList.razor @@ -0,0 +1,28 @@ +@page "/faqs" + +Häufige Fragen + +Häufige Fragen + +@if (_groups.Count == 0) +{ + Zurzeit gibt es keine Fragen. +} +else +{ + @foreach (var group in _groups) + { + + @TopicLabel(group.Key) + @foreach (var faq in group) + { + + @faq.Question + + @((MarkupString)Markdown.ToHtml(faq.Answer)) + + + } + + } +} diff --git a/Elternbeirat.Web/Features/Faqs/FaqList.razor.cs b/Elternbeirat.Web/Features/Faqs/FaqList.razor.cs new file mode 100644 index 0000000..90a2aed --- /dev/null +++ b/Elternbeirat.Web/Features/Faqs/FaqList.razor.cs @@ -0,0 +1,48 @@ +using Elternbeirat.Contracts; +using Elternbeirat.PocketBase; +using Microsoft.AspNetCore.Components; + +namespace Elternbeirat.Web.Features.Faqs; + +public partial class FaqList +{ + [LoggerMessage(Level = LogLevel.Warning, Message = "Could not load FAQs from PocketBase.")] + private static partial void LogLoadFailed(ILogger logger, Exception exception); + + // German heading per topic. Unknown topics fall back to the raw key so a new + // topic in PocketBase still shows up rather than being silently dropped. + private static readonly Dictionary TopicLabels = + new(StringComparer.Ordinal) + { + ["mensa"] = "Mensa und Mittagessen", + ["schliessfach"] = "Schließfächer", + ["elterneuro"] = "Eltern-Euro", + ["elternarbeit"] = "Elternarbeit und Mitmachen", + }; + + [Inject] + private PocketBaseClient PocketBase { get; set; } = default!; + + [Inject] + private ILogger Logger { get; set; } = default!; + + private IReadOnlyList> _groups = []; + + protected override async Task OnInitializedAsync() + { + try + { + var faqs = await PocketBase.GetFaqsAsync(); + _groups = [.. faqs.GroupBy(faq => faq.Topic)]; + } + catch (HttpRequestException ex) + { + // Source unreachable -> show an empty page instead of a 500. The page + // stays up; the "keine Fragen" note is rendered. + LogLoadFailed(Logger, ex); + } + } + + private static string TopicLabel(string topic) => + TopicLabels.TryGetValue(topic, out var label) ? label : topic; +} diff --git a/Elternbeirat.Web/Features/Faqs/FaqList.razor.css b/Elternbeirat.Web/Features/Faqs/FaqList.razor.css new file mode 100644 index 0000000..d8d3670 --- /dev/null +++ b/Elternbeirat.Web/Features/Faqs/FaqList.razor.css @@ -0,0 +1,22 @@ +.faq-topic { + margin-bottom: 2rem; +} + +.faq-topic h2 { + margin-bottom: 0.5rem; +} + +.faq-item { + border-bottom: 1px solid #eee; + padding: 0.5rem 0; +} + +.faq-item summary { + cursor: pointer; + font-weight: 600; +} + +.faq-answer { + padding: 0.5rem 0 0.25rem; + color: #333; +}
Zurzeit gibt es keine Fragen.