Define the all-day rule once as Event.HasTime
This commit is contained in:
1 parent
31b114982e
commit
66bc76af1c
4 files changed
+86
-56
No files matched your search
@@ -68,4 +68,16 @@ public record Event
|
||||
/// </summary>
|
||||
[JsonPropertyName("public")]
|
||||
public bool Public { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the event has a wall-clock time, as opposed
|
||||
/// to being an all-day event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PocketBase has no all-day flag; the convention is that a start of 00:00
|
||||
/// means all-day. This is the single definition of that rule -- formatting and
|
||||
/// the calendar feed both read it here instead of re-deriving it.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public bool HasTime => Start.TimeOfDay != TimeSpan.Zero;
|
||||
}
|
||||
@@ -14,6 +14,12 @@ namespace Elternbeirat.PocketBase;
|
||||
/// PocketBase via query parameters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Responses are deserialized with the web defaults. Date conversion from UTC
|
||||
/// to Berlin wall-clock time is declared on the contracts themselves via
|
||||
/// <c>[JsonConverter]</c> (e.g. <see cref="Event.Start"/>, <see cref="Post.Date"/>),
|
||||
/// so no serializer options are configured here.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Register the class as a typed client via <c>AddHttpClient</c>, with
|
||||
/// <see cref="HttpClient.BaseAddress"/> set to the PocketBase base URL.
|
||||
/// </para>
|
||||
@@ -23,11 +29,6 @@ namespace Elternbeirat.PocketBase;
|
||||
/// </param>
|
||||
public sealed class PocketBaseClient(HttpClient http)
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared serializer options for every request; see <see cref="CreateJsonOptions"/>.
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all public pages, ordered by <see cref="Page.Order"/>.
|
||||
/// </summary>
|
||||
@@ -82,18 +83,6 @@ public sealed class PocketBaseClient(HttpClient http)
|
||||
public Task<IReadOnlyList<Faq>> GetFaqsAsync(CancellationToken ct = default)
|
||||
=> GetRecordsAsync<Faq>("faqs", "topic", ct);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the serializer options. Date handling lives on the contracts
|
||||
/// themselves via <c>[JsonConverter]</c> on each date field (e.g.
|
||||
/// <see cref="Contracts.Event.Start"/>, <see cref="Contracts.Post.Date"/>), so
|
||||
/// nothing date-related is registered here.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The configured options.
|
||||
/// </returns>
|
||||
private static JsonSerializerOptions CreateJsonOptions() =>
|
||||
new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
/// <summary>
|
||||
/// Gets all public records of a collection in a single request.
|
||||
/// </summary>
|
||||
@@ -123,7 +112,7 @@ public sealed class PocketBaseClient(HttpClient http)
|
||||
+ $"?perPage=500&filter={Uri.EscapeDataString("public=true")}"
|
||||
+ $"&sort={Uri.EscapeDataString(sort)}";
|
||||
|
||||
var result = await http.GetFromJsonAsync<RecordList<T>>(url, JsonOptions, ct);
|
||||
var result = await http.GetFromJsonAsync<RecordList<T>>(url, ct);
|
||||
return result?.Items ?? [];
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public static class IcsCalendar
|
||||
// between requests, so caches and clients do not see spurious changes.
|
||||
AppendLine(sb, "DTSTAMP:20000101T000000Z");
|
||||
|
||||
if (HasTime(ev))
|
||||
if (ev.HasTime)
|
||||
{
|
||||
AppendLine(sb, $"DTSTART:{Local(ev.Start)}");
|
||||
if (ev.End is { } end)
|
||||
@@ -75,12 +75,6 @@ public static class IcsCalendar
|
||||
AppendLine(sb, "END:VEVENT");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An entry counts as all-day when its start has no wall-clock time (00:00).
|
||||
/// PocketBase has no separate flag; the convention is a midnight start.
|
||||
/// </summary>
|
||||
private static bool HasTime(Event ev) => ev.Start.TimeOfDay != TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// A UID that stays the same as long as the entry's title and start do, so a
|
||||
/// re-subscribe updates the event instead of creating a duplicate.
|
||||
|
||||
@@ -4,45 +4,80 @@ using Elternbeirat.Contracts;
|
||||
namespace Elternbeirat.Web.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Formats an <see cref="Event"/>'s date range for display in German. Shared by
|
||||
/// the full event list and the home-page events embed so both render dates the
|
||||
/// same way.
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public static class EventFormat
|
||||
{
|
||||
private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
|
||||
|
||||
// An event counts as all-day when its start carries no wall-clock time
|
||||
// (00:00); PocketBase has no separate flag.
|
||||
private static bool HasTime(Event ev) => ev.Start.TimeOfDay != TimeSpan.Zero;
|
||||
/// <summary>
|
||||
/// Date format for the start, and for a single-day event.
|
||||
/// </summary>
|
||||
private const string LongDate = "dddd, d. MMMM yyyy";
|
||||
|
||||
/// <summary>
|
||||
/// Formats the entry's date range: a single day, a date with a time, or a
|
||||
/// span across days.
|
||||
/// Date format for the end of a multi-day event; the weekday is omitted.
|
||||
/// </summary>
|
||||
public static string Range(Event ev)
|
||||
{
|
||||
var start = HasTime(ev)
|
||||
? ev.Start.ToString("dddd, d. MMMM yyyy, HH:mm", German) + " Uhr"
|
||||
: ev.Start.ToString("dddd, d. MMMM yyyy", German);
|
||||
private const string ShortDate = "d. MMMM yyyy";
|
||||
|
||||
if (ev.End is not { } end)
|
||||
{
|
||||
return start;
|
||||
}
|
||||
/// <summary>
|
||||
/// The culture used for month and weekday names.
|
||||
/// </summary>
|
||||
private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
|
||||
|
||||
// Same day: append just the end time. Different days: append the full
|
||||
// end date.
|
||||
if (end.Date == ev.Start.Date)
|
||||
/// <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
|
||||
{
|
||||
return HasTime(ev)
|
||||
? start + "–" + end.ToString("HH:mm", German) + " Uhr"
|
||||
: start;
|
||||
}
|
||||
(var start, null, var hasTime)
|
||||
=> Stamp(start, LongDate, hasTime),
|
||||
(var start, { } end, true) when end.Date == start.Date
|
||||
=> $"{start.ToString(LongDate, German)}, {start.ToString("HH:mm", German)}–{end.ToString("HH:mm", 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)}",
|
||||
};
|
||||
|
||||
var endText = HasTime(ev)
|
||||
? end.ToString("d. MMMM yyyy, HH:mm", German) + " Uhr"
|
||||
: end.ToString("d. MMMM yyyy", German);
|
||||
return start + " – " + endText;
|
||||
}
|
||||
/// <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", German) + " Uhr"
|
||||
: value.ToString(dateFormat, German);
|
||||
}
|
||||
Reference in new issue
Block a user