diff --git a/Elternbeirat.Web/Components/Pages/PostDetail.razor b/Elternbeirat.Web/Components/Pages/PostDetail.razor
deleted file mode 100644
index 5e678a7..0000000
--- a/Elternbeirat.Web/Components/Pages/PostDetail.razor
+++ /dev/null
@@ -1,46 +0,0 @@
-@page "/beitraege/{Slug}"
-@using System.Globalization
-@using Elternbeirat.Web.Services
-@inject PostService PostService
-
-@if (_post is null)
-{
- Nicht gefunden
-}
-else
-{
- @_post.Title
-
-
- ← Alle Beiträge
-
-
- @((MarkupString)_post.ContentHtml)
-
-}
-
-@code {
- private static readonly CultureInfo _german = CultureInfo.GetCultureInfo("de-DE");
-
- [Parameter]
- public string Slug { get; set; } = "";
-
- private Post? _post;
-
- protected override void OnParametersSet()
- {
- _post = PostService.Find(Slug);
-
- // Unknown slug -> 404, so UseStatusCodePagesWithReExecute serves the
- // /not-found page instead of an empty 200 response.
- if (_post is null && HttpContext is not null)
- {
- HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
- }
- }
-
- [CascadingParameter]
- private HttpContext? HttpContext { get; set; }
-}
diff --git a/Elternbeirat.Web/Services/Event.cs b/Elternbeirat.Web/Features/Events/Event.cs
similarity index 100%
rename from Elternbeirat.Web/Services/Event.cs
rename to Elternbeirat.Web/Features/Events/Event.cs
diff --git a/Elternbeirat.Web/Components/Pages/EventList.razor b/Elternbeirat.Web/Features/Events/EventList.razor
similarity index 59%
rename from Elternbeirat.Web/Components/Pages/EventList.razor
rename to Elternbeirat.Web/Features/Events/EventList.razor
index 0fdee26..5d71ca1 100644
--- a/Elternbeirat.Web/Components/Pages/EventList.razor
+++ b/Elternbeirat.Web/Features/Events/EventList.razor
@@ -1,6 +1,4 @@
@page "/termine"
-@using System.Globalization
-@using Elternbeirat.Web.Services
@inject EventService EventService
Termine
@@ -57,35 +55,3 @@ else
}
}
-
-@code {
- private static readonly CultureInfo _german = CultureInfo.GetCultureInfo("de-DE");
-
- // Formats the entry's date range for display: a single day, a date with a
- // time, or a span across days.
- private static string Format(Event ev)
- {
- var start = ev.HasTime
- ? ev.Start.ToString("dddd, d. MMMM yyyy, HH:mm", _german) + " Uhr"
- : ev.Start.ToString("dddd, d. MMMM yyyy", _german);
-
- if (ev.End is not { } end)
- {
- return start;
- }
-
- // Same day: append just the end time. Different days: append the full
- // end date.
- if (end.Date == ev.Start.Date)
- {
- return ev.HasTime
- ? start + "–" + end.ToString("HH:mm", _german) + " Uhr"
- : start;
- }
-
- var endText = ev.HasTime
- ? end.ToString("d. MMMM yyyy, HH:mm", _german) + " Uhr"
- : end.ToString("d. MMMM yyyy", _german);
- return start + " – " + endText;
- }
-}
diff --git a/Elternbeirat.Web/Features/Events/EventList.razor.cs b/Elternbeirat.Web/Features/Events/EventList.razor.cs
new file mode 100644
index 0000000..a17a7bd
--- /dev/null
+++ b/Elternbeirat.Web/Features/Events/EventList.razor.cs
@@ -0,0 +1,37 @@
+using System.Globalization;
+using Elternbeirat.Web.Services;
+
+namespace Elternbeirat.Web.Features.Events;
+
+public partial class EventList
+{
+ private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
+
+ // Formats the entry's date range for display: a single day, a date with a
+ // time, or a span across days.
+ private static string Format(Event ev)
+ {
+ var start = ev.HasTime
+ ? ev.Start.ToString("dddd, d. MMMM yyyy, HH:mm", German) + " Uhr"
+ : ev.Start.ToString("dddd, d. MMMM yyyy", German);
+
+ if (ev.End is not { } end)
+ {
+ return start;
+ }
+
+ // Same day: append just the end time. Different days: append the full
+ // end date.
+ if (end.Date == ev.Start.Date)
+ {
+ return ev.HasTime
+ ? start + "–" + end.ToString("HH:mm", German) + " Uhr"
+ : start;
+ }
+
+ var endText = ev.HasTime
+ ? end.ToString("d. MMMM yyyy, HH:mm", German) + " Uhr"
+ : end.ToString("d. MMMM yyyy", German);
+ return start + " – " + endText;
+ }
+}
diff --git a/Elternbeirat.Web/Features/Events/EventList.razor.css b/Elternbeirat.Web/Features/Events/EventList.razor.css
new file mode 100644
index 0000000..1b33ecc
--- /dev/null
+++ b/Elternbeirat.Web/Features/Events/EventList.razor.css
@@ -0,0 +1,42 @@
+.event-list {
+ list-style: none;
+ padding: 0;
+}
+
+.event-list li {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #eee;
+}
+
+.event-list time {
+ display: block;
+ color: #777;
+ font-size: 0.85rem;
+}
+
+.event-title {
+ font-size: 1.1rem;
+ font-weight: 600;
+}
+
+.event-location::before {
+ content: " · ";
+ color: #777;
+}
+
+.event-location {
+ color: #555;
+}
+
+.event-note {
+ margin: 0.25rem 0 0;
+ color: #444;
+}
+
+.event-list-past {
+ color: #777;
+}
+
+.event-list-past .event-title {
+ font-weight: 400;
+}
diff --git a/Elternbeirat.Web/Services/EventService.cs b/Elternbeirat.Web/Features/Events/EventService.cs
similarity index 100%
rename from Elternbeirat.Web/Services/EventService.cs
rename to Elternbeirat.Web/Features/Events/EventService.cs
diff --git a/Elternbeirat.Web/Services/IcsCalendar.cs b/Elternbeirat.Web/Features/Events/IcsCalendar.cs
similarity index 100%
rename from Elternbeirat.Web/Services/IcsCalendar.cs
rename to Elternbeirat.Web/Features/Events/IcsCalendar.cs
diff --git a/Elternbeirat.Web/Components/Pages/Home.razor b/Elternbeirat.Web/Features/Home/Home.razor
similarity index 65%
rename from Elternbeirat.Web/Components/Pages/Home.razor
rename to Elternbeirat.Web/Features/Home/Home.razor
index 829e27e..b811c0e 100644
--- a/Elternbeirat.Web/Components/Pages/Home.razor
+++ b/Elternbeirat.Web/Features/Home/Home.razor
@@ -1,6 +1,4 @@
@page "/"
-@using System.Globalization
-@using Elternbeirat.Web.Services
@inject PostService PostService
Elternbeirat der IGMH
@@ -27,7 +25,7 @@
@post.Title
}
@@ -35,15 +33,3 @@
Alle Beiträge →
}
-
-@code {
- private static readonly CultureInfo _german = CultureInfo.GetCultureInfo("de-DE");
-
- private IReadOnlyList _latest = [];
-
- protected override void OnInitialized()
- {
- // Show the three most recent posts as a teaser on the home page.
- _latest = PostService.All.Take(3).ToList();
- }
-}
diff --git a/Elternbeirat.Web/Features/Home/Home.razor.cs b/Elternbeirat.Web/Features/Home/Home.razor.cs
new file mode 100644
index 0000000..8ad1eef
--- /dev/null
+++ b/Elternbeirat.Web/Features/Home/Home.razor.cs
@@ -0,0 +1,17 @@
+using System.Globalization;
+using Elternbeirat.Web.Services;
+
+namespace Elternbeirat.Web.Features.Home;
+
+public partial class Home
+{
+ private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
+
+ private IReadOnlyList _latest = [];
+
+ protected override void OnInitialized()
+ {
+ // Show the three most recent posts as a teaser on the home page.
+ _latest = [.. PostService.All.Take(3)];
+ }
+}
diff --git a/Elternbeirat.Web/Features/Home/Home.razor.css b/Elternbeirat.Web/Features/Home/Home.razor.css
new file mode 100644
index 0000000..016ae11
--- /dev/null
+++ b/Elternbeirat.Web/Features/Home/Home.razor.css
@@ -0,0 +1,29 @@
+.intro {
+ font-size: 1.1rem;
+ color: #444;
+}
+
+.home-news {
+ margin-top: 2rem;
+}
+
+.post-list {
+ list-style: none;
+ padding: 0;
+}
+
+.post-list li {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #eee;
+}
+
+.post-list a {
+ font-size: 1.1rem;
+ text-decoration: none;
+}
+
+.post-list time {
+ display: block;
+ color: #777;
+ font-size: 0.85rem;
+}
diff --git a/Elternbeirat.Web/Features/Pages/ContentPage.razor b/Elternbeirat.Web/Features/Pages/ContentPage.razor
new file mode 100644
index 0000000..dd09e47
--- /dev/null
+++ b/Elternbeirat.Web/Features/Pages/ContentPage.razor
@@ -0,0 +1,14 @@
+@page "/{Slug}"
+@inject PageService PageService
+
+@if (_page is null)
+{
+ Nicht gefunden
+}
+else
+{
+ @_page.Title
+
+ @((MarkupString)_page.ContentHtml)
+
+}
diff --git a/Elternbeirat.Web/Components/Pages/ContentPage.razor b/Elternbeirat.Web/Features/Pages/ContentPage.razor.cs
similarity index 67%
rename from Elternbeirat.Web/Components/Pages/ContentPage.razor
rename to Elternbeirat.Web/Features/Pages/ContentPage.razor.cs
index 76f4b2e..0d37292 100644
--- a/Elternbeirat.Web/Components/Pages/ContentPage.razor
+++ b/Elternbeirat.Web/Features/Pages/ContentPage.razor.cs
@@ -1,20 +1,11 @@
-@page "/{Slug}"
-@using Elternbeirat.Web.Services
-@inject PageService PageService
+using Elternbeirat.Web.Services;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Http;
-@if (_page is null)
-{
- Nicht gefunden
-}
-else
-{
- @_page.Title
-
- @((MarkupString)_page.ContentHtml)
-
-}
+namespace Elternbeirat.Web.Features.Pages;
-@code {
+public partial class ContentPage
+{
[Parameter]
public string Slug { get; set; } = "";
diff --git a/Elternbeirat.Web/Services/Page.cs b/Elternbeirat.Web/Features/Pages/Page.cs
similarity index 100%
rename from Elternbeirat.Web/Services/Page.cs
rename to Elternbeirat.Web/Features/Pages/Page.cs
diff --git a/Elternbeirat.Web/Services/PageService.cs b/Elternbeirat.Web/Features/Pages/PageService.cs
similarity index 100%
rename from Elternbeirat.Web/Services/PageService.cs
rename to Elternbeirat.Web/Features/Pages/PageService.cs
diff --git a/Elternbeirat.Web/Services/Post.cs b/Elternbeirat.Web/Features/Posts/Post.cs
similarity index 100%
rename from Elternbeirat.Web/Services/Post.cs
rename to Elternbeirat.Web/Features/Posts/Post.cs
diff --git a/Elternbeirat.Web/Features/Posts/PostDetail.razor b/Elternbeirat.Web/Features/Posts/PostDetail.razor
new file mode 100644
index 0000000..ef31e63
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostDetail.razor
@@ -0,0 +1,20 @@
+@page "/beitraege/{Slug}"
+@inject PostService PostService
+
+@if (_post is null)
+{
+ Nicht gefunden
+}
+else
+{
+ @_post.Title
+
+
+ ← Alle Beiträge
+
+
+ @((MarkupString)_post.ContentHtml)
+
+}
diff --git a/Elternbeirat.Web/Features/Posts/PostDetail.razor.cs b/Elternbeirat.Web/Features/Posts/PostDetail.razor.cs
new file mode 100644
index 0000000..e2d1e99
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostDetail.razor.cs
@@ -0,0 +1,31 @@
+using System.Globalization;
+using Elternbeirat.Web.Services;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Http;
+
+namespace Elternbeirat.Web.Features.Posts;
+
+public partial class PostDetail
+{
+ private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
+
+ [Parameter]
+ public string Slug { get; set; } = "";
+
+ private Post? _post;
+
+ protected override void OnParametersSet()
+ {
+ _post = PostService.Find(Slug);
+
+ // Unknown slug -> 404, so UseStatusCodePagesWithReExecute serves the
+ // /not-found page instead of an empty 200 response.
+ if (_post is null && HttpContext is not null)
+ {
+ HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
+ }
+ }
+
+ [CascadingParameter]
+ private HttpContext? HttpContext { get; set; }
+}
diff --git a/Elternbeirat.Web/Features/Posts/PostDetail.razor.css b/Elternbeirat.Web/Features/Posts/PostDetail.razor.css
new file mode 100644
index 0000000..f9ff28c
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostDetail.razor.css
@@ -0,0 +1,13 @@
+.post-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.post-meta time {
+ display: block;
+ color: #777;
+ font-size: 0.85rem;
+}
diff --git a/Elternbeirat.Web/Components/Pages/PostList.razor b/Elternbeirat.Web/Features/Posts/PostList.razor
similarity index 68%
rename from Elternbeirat.Web/Components/Pages/PostList.razor
rename to Elternbeirat.Web/Features/Posts/PostList.razor
index 1b2ee6c..0301a89 100644
--- a/Elternbeirat.Web/Components/Pages/PostList.razor
+++ b/Elternbeirat.Web/Features/Posts/PostList.razor
@@ -1,6 +1,4 @@
@page "/beitraege"
-@using System.Globalization
-@using Elternbeirat.Web.Services
@inject PostService PostService
Beiträge
@@ -19,13 +17,9 @@ else
@post.Title
}
}
-
-@code {
- private static readonly CultureInfo _german = CultureInfo.GetCultureInfo("de-DE");
-}
diff --git a/Elternbeirat.Web/Features/Posts/PostList.razor.cs b/Elternbeirat.Web/Features/Posts/PostList.razor.cs
new file mode 100644
index 0000000..f582b7e
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostList.razor.cs
@@ -0,0 +1,8 @@
+using System.Globalization;
+
+namespace Elternbeirat.Web.Features.Posts;
+
+public partial class PostList
+{
+ private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
+}
diff --git a/Elternbeirat.Web/Features/Posts/PostList.razor.css b/Elternbeirat.Web/Features/Posts/PostList.razor.css
new file mode 100644
index 0000000..ac756fa
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostList.razor.css
@@ -0,0 +1,20 @@
+.post-list {
+ list-style: none;
+ padding: 0;
+}
+
+.post-list li {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #eee;
+}
+
+.post-list a {
+ font-size: 1.1rem;
+ text-decoration: none;
+}
+
+.post-list time {
+ display: block;
+ color: #777;
+ font-size: 0.85rem;
+}
diff --git a/Elternbeirat.Web/Services/PostService.cs b/Elternbeirat.Web/Features/Posts/PostService.cs
similarity index 100%
rename from Elternbeirat.Web/Services/PostService.cs
rename to Elternbeirat.Web/Features/Posts/PostService.cs
diff --git a/Elternbeirat.Web/Services/FrontMatter.cs b/Elternbeirat.Web/Shared/FrontMatter.cs
similarity index 100%
rename from Elternbeirat.Web/Services/FrontMatter.cs
rename to Elternbeirat.Web/Shared/FrontMatter.cs
diff --git a/Elternbeirat.Web/_Imports.razor b/Elternbeirat.Web/_Imports.razor
new file mode 100644
index 0000000..57b29e7
--- /dev/null
+++ b/Elternbeirat.Web/_Imports.razor
@@ -0,0 +1,10 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using Elternbeirat.Web
+@using Elternbeirat.Web.Services
diff --git a/Elternbeirat.Web/wwwroot/app.css b/Elternbeirat.Web/wwwroot/app.css
index 161563d..e49babf 100644
--- a/Elternbeirat.Web/wwwroot/app.css
+++ b/Elternbeirat.Web/wwwroot/app.css
@@ -22,91 +22,6 @@ h1:focus {
outline: none;
}
-/* Home page */
-.intro {
- font-size: 1.1rem;
- color: #444;
-}
-
-.home-news {
- margin-top: 2rem;
-}
-
-/* News list on /beitraege */
-.post-list {
- list-style: none;
- padding: 0;
-}
-
-.post-list li {
- padding: 0.75rem 0;
- border-bottom: 1px solid #eee;
-}
-
-.post-list a {
- font-size: 1.1rem;
- text-decoration: none;
-}
-
-.post-list time,
-.post-meta time {
- display: block;
- color: #777;
- font-size: 0.85rem;
-}
-
-.post-meta {
- display: flex;
- justify-content: space-between;
- align-items: baseline;
- gap: 1rem;
- margin-bottom: 1rem;
-}
-
-/* Event list on /termine */
-.event-list {
- list-style: none;
- padding: 0;
-}
-
-.event-list li {
- padding: 0.75rem 0;
- border-bottom: 1px solid #eee;
-}
-
-.event-list time {
- display: block;
- color: #777;
- font-size: 0.85rem;
-}
-
-.event-title {
- font-size: 1.1rem;
- font-weight: 600;
-}
-
-.event-location::before {
- content: " · ";
- color: #777;
-}
-
-.event-location {
- color: #555;
-}
-
-.event-note {
- margin: 0.25rem 0 0;
- color: #444;
-}
-
-.event-list-past {
- color: #777;
-}
-
-.event-list-past .event-title {
- font-weight: 400;
-}
-
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
@@ -140,4 +55,4 @@ h1:focus {
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
-}
\ No newline at end of file
+}