Extract date rendering into per-feature Date components

This commit is contained in:
tleininger committed 2026-09-25 08:49:44 +02:00
1 parent d95bf0dcbe
commit 455b603299
16 files changed
+168 -71

No files matched your search

@@ -0,0 +1,10 @@
@*
Renders an event's date as a <time> element: the start in the machine-readable
datetime attribute, the full German range (weekday, clock time, multi-day span)
as visible text via EventDateFormat.
Unlike <PostDate>, this takes the whole Event, not a plain day: the visible
range depends on the event's end and whether it has a clock time, and that
logic already lives in EventDateFormat.
*@
<time datetime="@Value.Start.ToString("s")">@EventDateFormat.Range(Value)</time>
@@ -0,0 +1,18 @@
using Elternbeirat.Contracts;
using Microsoft.AspNetCore.Components;
namespace Elternbeirat.Web.Features.Events;
/// <summary>
/// Renders an event's date range as a <c>&lt;time&gt;</c> element, in both a
/// machine-readable and a visitor-facing form.
/// </summary>
public partial class EventDate
{
/// <summary>
/// The event whose date range to render.
/// </summary>
[Parameter]
[EditorRequired]
public Event Value { get; set; } = null!;
}
@@ -0,0 +1,83 @@
using Elternbeirat.Contracts;
using Elternbeirat.Web.Shared;
namespace Elternbeirat.Web.Features.Events;
/// <summary>
/// Formats the date range of an <see cref="Event"/> for display in German.
/// </summary>
/// <remarks>
/// Shared by the full event list and the home-page events embed, so both render
/// dates the same way.
/// <para>
/// This is the event counterpart to
/// <see cref="Posts.PostDateFormat"/>, which formats a plain
/// <see cref="DateOnly"/> day instead of a point-in-time range.
/// </para>
/// </remarks>
public static class EventDateFormat
{
/// <summary>
/// Date format for the start, and for a single-day event.
/// </summary>
private const string LongDate = "dddd, d. MMMM yyyy";
/// <summary>
/// Date format for the end of a multi-day event; the weekday is omitted.
/// </summary>
private const string ShortDate = "d. MMMM yyyy";
/// <summary>
/// Formats the date range of an event.
/// </summary>
/// <param name="event">
/// The event to format.
/// </param>
/// <returns>
/// The formatted range, depending on <see cref="Event.End"/> and <see cref="Event.HasTime"/>:
/// <list type="bullet">
/// <item>
/// <description>No end: <c>Donnerstag, 8. Oktober 2026, 19:30 Uhr</c></description>
/// </item>
/// <item>
/// <description>Same day: <c>Donnerstag, 8. Oktober 2026, 19:30–21:00 Uhr</c></description>
/// </item>
/// <item>
/// <description>Several days: <c>Freitag, 9. Oktober 2026 – 11. Oktober 2026</c></description>
/// </item>
/// </list>
/// Without a time, the time parts are omitted.
/// </returns>
public static string Range(Event @event)
=> (@event.Start, @event.End, @event.HasTime) switch
{
(var start, null, var hasTime)
=> Stamp(start, LongDate, hasTime),
(var start, { } end, true) when end.Date == start.Date
=> $"{start.ToString(LongDate, Cultures.German)}, {start.ToString("HH:mm", Cultures.German)}–{end.ToString("HH:mm", Cultures.German)} Uhr",
(var start, { } end, false) when end.Date == start.Date
=> Stamp(start, LongDate, withTime: false),
(var start, { } end, var hasTime)
=> $"{Stamp(start, LongDate, hasTime)} – {Stamp(end, ShortDate, hasTime)}",
};
/// <summary>
/// Formats a single point in time as a date with an optional time.
/// </summary>
/// <param name="value">
/// The date and time to format.
/// </param>
/// <param name="dateFormat">
/// The date format, e.g. <see cref="LongDate"/>.
/// </param>
/// <param name="withTime">
/// <see langword="true"/> to append the time, e.g. <c>, 19:30 Uhr</c>.
/// </param>
/// <returns>
/// The formatted date, with the time if requested.
/// </returns>
private static string Stamp(DateTime value, string dateFormat, bool withTime)
=> withTime
? value.ToString(dateFormat + ", HH:mm", Cultures.German) + " Uhr"
: value.ToString(dateFormat, Cultures.German);
}
@@ -18,17 +18,17 @@ else
<h2>Kommende Termine</h2>
@if (_upcoming.Count == 0)
@if (_upcomingEvents.Count == 0)
{
<p>Zurzeit sind keine Termine geplant.</p>
}
else
{
<ul class="event-list">
@foreach (var ev in _upcoming)
@foreach (var ev in _upcomingEvents)
{
<li>
<time datetime="@ev.Start.ToString("s")">@EventFormat.Range(ev)</time>
<EventDate Value="ev" />
<span class="event-title">@ev.Title</span>
@if (!string.IsNullOrWhiteSpace(ev.Location))
{
@@ -43,14 +43,14 @@ else
</ul>
}
@if (_past.Count > 0)
@if (_pastEvents.Count > 0)
{
<h2>Vergangene Termine</h2>
<ul class="event-list event-list-past">
@foreach (var ev in _past)
@foreach (var ev in _pastEvents)
{
<li>
<time datetime="@ev.Start.ToString("s")">@EventFormat.Range(ev)</time>
<EventDate Value="ev" />
<span class="event-title">@ev.Title</span>
@if (!string.IsNullOrWhiteSpace(ev.Location))
{
@@ -23,8 +23,8 @@ public partial class EventList
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private IReadOnlyList<Event> _upcoming = [];
private IReadOnlyList<Event> _past = [];
private IReadOnlyList<Event> _upcomingEvents = [];
private IReadOnlyList<Event> _pastEvents = [];
// Set when the content source could not be reached or read. The markup then
// shows a "temporarily unavailable" note instead of "no events", matching the
@@ -51,13 +51,13 @@ public partial class EventList
// Split into upcoming and past by date, so an event earlier today still
// counts as upcoming. Upcoming earliest first, past most recent first.
var today = DateOnly.FromDateTime(DateTime.Today);
_upcoming =
_upcomingEvents =
[
.. events
.Where(@event => DateOnly.FromDateTime(@event.Start) >= today)
.OrderBy(@event => @event.Start)
];
_past =
_pastEvents =
[
.. events
.Where(@event => DateOnly.FromDateTime(@event.Start) < today)