Read all content from PocketBase, per request #30

Merged
Tom merged 7 commits from feature/pocketbase-consume into main 2026-09-23 16:02:46 +02:00
7 changed files with 127 additions and 7 deletions
Showing only changes of commit 6b5d9dda55 - Show all commits

No files matched your search

+9 -4
View File
@@ -7,10 +7,15 @@
<list />
</option>
<option name="commandLineOptions" value="--build" />
<!-- Base compose (loaded first); the dev overlay in sourceFilePath wins. -->
<!-- Dev overlay: applied LAST so it wins (host ports, local web build, and
the pb_data_dev volume replacing the Unraid bind mount). In Rider the
sourceFilePath is merged first and secondarySourceFiles after it, so
the overlay must sit in secondarySourceFiles, not the other way round --
otherwise the base file overrides it and PocketBase mounts the empty
/mnt/user prod path locally. -->
<option name="secondarySourceFiles">
<list>
<option value="$PROJECT_DIR$/compose.yaml" />
<option value="$PROJECT_DIR$/compose.dev.yaml" />
</list>
</option>
<option name="services">
@@ -19,8 +24,8 @@
<option value="eb-web" />
</list>
</option>
<!-- Dev overlay (primary = last applied): host ports + local web build. -->
<option name="sourceFilePath" value="$PROJECT_DIR$/compose.dev.yaml" />
<!-- Base compose (production-like), merged first. -->
<option name="sourceFilePath" value="$PROJECT_DIR$/compose.yaml" />
<option name="upForceRecreate" value="true" />
</settings>
</deployment>
+3 -1
View File
@@ -260,10 +260,12 @@ public sealed class PocketBaseFixture : IAsyncLifetime
title = "Förderverein", body = "# Förderverein", location = "header",
order = 3, slug = "patrons", embed = Array.Empty<string>(), @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<string>(), @public = true,
order = 4, slug = "faqs", embed = Array.Empty<string>(), @public = true,
});
await CreateRecordAsync(http, "pages", new
{
+16 -1
View File
@@ -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
}
}
@@ -10,7 +10,7 @@
<NavLink href="/patrons">Förderverein</NavLink>
<NavLink href="/posts">Beiträge</NavLink>
<NavLink href="/events">Termine</NavLink>
<NavLink href="/faq">FAQ</NavLink>
<NavLink href="/faqs">FAQ</NavLink>
<NavLink href="/downloads">Downloads</NavLink>
<NavLink href="/contact">Kontakt</NavLink>
</nav>
@@ -0,0 +1,28 @@
@page "/faqs"
<PageTitle>Häufige Fragen</PageTitle>
<h1>Häufige Fragen</h1>
@if (_groups.Count == 0)
{
<p>Zurzeit gibt es keine Fragen.</p>
}
else
{
@foreach (var group in _groups)
{
<section class="faq-topic">
<h2>@TopicLabel(group.Key)</h2>
@foreach (var faq in group)
{
<details class="faq-item">
<summary>@faq.Question</summary>
<div class="faq-answer">
@((MarkupString)Markdown.ToHtml(faq.Answer))
</div>
</details>
}
</section>
}
}
@@ -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<string, string> 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<FaqList> Logger { get; set; } = default!;
private IReadOnlyList<IGrouping<string, Faq>> _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;
}
@@ -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;
}