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 a post's calendar day as a <time> element: the ISO value in the
machine-readable datetime attribute, the German long form as visible text.
A <time> needs the day in both forms, so putting it here keeps every caller
to a single tag instead of repeating the two PostDateFormat calls.
The event counterpart is <EventDate>, which renders a point in time (with an
optional clock time and range) instead of a plain day.
*@
<time datetime="@PostDateFormat.Iso(Value)">@PostDateFormat.Display(Value)</time>
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Components;
namespace Elternbeirat.Web.Features.Posts;
/// <summary>
/// Renders a post's publication day as a <c>&lt;time&gt;</c> element, in both a
/// machine-readable and a visitor-facing form. The two formats come from
/// <see cref="PostDateFormat"/>.
/// </summary>
public partial class PostDate
{
/// <summary>
/// The post's publication day.
/// </summary>
[Parameter]
[EditorRequired]
public DateOnly Value { get; set; }
}
@@ -0,0 +1,9 @@
/* The date's own look: small and softened. Lives with the component so every
place that renders <PostDate> (list, home embed, detail page) shows the date
the same way. Positioning (margin to neighbours) is NOT set here -- that
depends on the surrounding context and is set by each caller. */
time {
display: block;
color: var(--color-text-soft); /* token, so it adapts in dark mode */
font-size: 0.85rem;
}
@@ -0,0 +1,55 @@
using System.Globalization;
using Elternbeirat.Web.Shared;
namespace Elternbeirat.Web.Features.Posts;
/// <summary>
/// Formats a post's calendar day for display and for a machine-readable
/// <c>datetime</c> attribute.
/// </summary>
/// <remarks>
/// Posts carry a <see cref="DateOnly"/> (a calendar day, no time of day). Their
/// <c>&lt;time&gt;</c> element needs the day twice: once in German for the
/// visitor (<see cref="Display"/>) and once as ISO 8601 for the <c>datetime</c>
/// attribute (<see cref="Iso"/>). Keeping both here means the two never drift
/// apart and the markup no longer repeats the same <c>ToString</c> pair.
/// <para>
/// This is the post counterpart to
/// <see cref="Events.EventDateFormat"/>, which formats a
/// <see cref="System.DateTime"/> range instead.
/// </para>
/// </remarks>
public static class PostDateFormat
{
/// <summary>
/// Display format for a calendar day, e.g. <c>8. Oktober 2026</c>. The
/// weekday is omitted, unlike the event format.
/// </summary>
private const string LongDate = "d. MMMM yyyy";
/// <summary>
/// Formats a calendar day in German for a visitor to read.
/// </summary>
/// <param name="value">
/// The calendar day to format.
/// </param>
/// <returns>
/// The day as e.g. <c>8. Oktober 2026</c>, using German month names.
/// </returns>
public static string Display(DateOnly value)
=> value.ToString(LongDate, Cultures.German);
/// <summary>
/// Formats a calendar day as ISO 8601 (<c>yyyy-MM-dd</c>) for a
/// machine-readable <c>datetime</c> attribute.
/// </summary>
/// <param name="value">
/// The calendar day to format.
/// </param>
/// <returns>
/// The day as e.g. <c>2026-10-08</c>, culture-invariant so the value is
/// stable regardless of the request's culture.
/// </returns>
public static string Iso(DateOnly value)
=> value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
@@ -12,10 +12,11 @@ else if (_post is not null)
<a href="/posts">&larr; Alle Beiträge</a>
</nav>
<article>
<time class="post-meta" datetime="@_post.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)">
@_post.Date.ToString("d. MMMM yyyy", Cultures.German)
</time>
@Body
<div class="post-content">
<PostDate Value="_post.Date" />
<h2>@_post.Title</h2>
@Body
</div>
</article>
}
@* No post and not unavailable: the slug was unknown; NavigationManager.NotFound()
@@ -1,7 +1,15 @@
.post-meta {
display: block;
color: var(--color-text-soft); /* token, so it adapts in dark mode */
font-size: 0.85rem;
margin-top: 1.5rem; /* space from the "all posts" link above */
margin-bottom: 0.25rem; /* keep the body close below */
/* Push the whole article body down from the "all posts" link above it. Wraps the
date, heading and body, so scoped CSS applies (the date sits in the PostDate
child component and would not be reachable on its own). The date's own look
comes from PostDate.razor.css. */
.post-content {
margin-top: 1.5rem;
}
/* The post title is the first h2 in the content, sitting right under its date, so
drop the large default heading gap above it (h1..h4 carry --space-5 top margin
globally); date and title belong together. Scoped to :first-of-type so h2
subheadings inside the Markdown body keep their normal spacing. */
.post-content h2:first-of-type {
margin-top: var(--space-1);
}
@@ -19,9 +19,7 @@ else
{
<li>
<a href="@($"/posts/{post.Slug}")">@post.Title</a>
<time datetime="@post.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)">
@post.Date.ToString("d. MMMM yyyy", Cultures.German)
</time>
<PostDate Value="post.Date" />
</li>
}
</ul>