import { berlinDay, formatTime, TIME_ZONE } from "@/lib/dates"; import { effectiveStart, isRecurring, type Sort } from "@/lib/filter"; import type { CalendarEvent } from "@/lib/types"; /** * How an event reads in the list. Pure, so the German wording is testable. * All of it is about the *effective* occurrence: a birthday shows this year's * date, not the year of birth. */ const LOCALE = "de-DE"; const part = (options: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat(LOCALE, { timeZone: TIME_ZONE, ...options }); const weekdayFormat = part({ weekday: "short" }); const dayFormat = part({ day: "numeric" }); const monthFormat = part({ month: "short" }); const dayMonthFormat = part({ day: "numeric", month: "short" }); export interface DateBlock { /** "Sa" */ weekday: string; /** "3" */ day: string; /** "Okt" */ month: string; } export const dateBlock = (instant: Date): DateBlock => ({ weekday: weekdayFormat.format(instant), day: dayFormat.format(instant), month: monthFormat.format(instant), }); /** The occurrence the list shows: start moved to the next repeat, end moved by the same amount. */ export const occurrence = (event: CalendarEvent, now: Date): { start: Date; end: Date } => { const start = effectiveStart(event, now); const shift = start.getTime() - event.start.getTime(); return { start, end: new Date(event.end.getTime() + shift) }; }; /** * "19:00–21:00 Uhr", "Ganztägig", "Ganztägig · 17.–19. Okt.", * "3. Okt., 19:00 – 5. Okt., 12:00". */ export const describeWhen = (event: CalendarEvent, now: Date): string => { const { start, end } = occurrence(event, now); const sameDay = berlinDay(start) === berlinDay(end); if (event.wholeDay) { return sameDay ? "Ganztägig" : `Ganztägig · ${dayMonthFormat.formatRange(start, end)}`; } if (sameDay) { return `${formatTime(start)}–${formatTime(end)} Uhr`; } return `${dayMonthFormat.format(start)}, ${formatTime(start)} – ${dayMonthFormat.format(end)}, ${formatTime(end)}`; }; /** Over before today (Berlin). Repeating events never are. */ export const isPast = (event: CalendarEvent, now: Date): boolean => !isRecurring(event) && berlinDay(event.end) < berlinDay(now); /** * Where "today" falls in a list sorted by start: the index of the event the * "Heute" marker goes in front of, or `events.length` for after the last one. * Ascending, that is the first event not yet over; descending, the first one * that is. Null for a list that isn't ordered by start, or an empty one. */ export const todayMarkerIndex = (events: readonly CalendarEvent[], sort: Sort, now: Date): number | null => { if (events.length === 0 || sort.startsWith("created")) { return null; } const lookingFor = sort === "start_desc"; const index = events.findIndex((e) => isPast(e, now) === lookingFor); return index === -1 ? events.length : index; }; /** The badge for a repeat frequency; "" for none. Unknown values are shown as they are. */ export const repeatLabel = (repeatFrequency: string): string => { switch (repeatFrequency) { case "": return ""; case "YEARLY": return "jährlich"; case "MONTHLY": return "monatlich"; case "WEEKLY": return "wöchentlich"; case "DAILY": return "täglich"; default: return repeatFrequency.toLowerCase(); } };