Read all content from PocketBase, per request #30
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user