From 0703704307b8847722fba7d96612f084118c010a Mon Sep 17 00:00:00 2001 From: Benno Tielen Date: Thu, 16 Jul 2026 09:22:46 +0200 Subject: [PATCH] fix: format date --- src/components/EventRow/EventRow.stories.tsx | 11 +++++ src/components/EventRow/EventRow.tsx | 8 +-- src/utils/formatEventDate.test.ts | 52 ++++++++++++++++++++ src/utils/formatEventDate.ts | 39 +++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 src/utils/formatEventDate.test.ts create mode 100644 src/utils/formatEventDate.ts diff --git a/src/components/EventRow/EventRow.stories.tsx b/src/components/EventRow/EventRow.stories.tsx index be9424a..f14c293 100644 --- a/src/components/EventRow/EventRow.stories.tsx +++ b/src/components/EventRow/EventRow.stories.tsx @@ -29,6 +29,17 @@ export const EventWithEnd: Story = { }, } +export const MultiDayEvent: Story = { + args: { + date: '2026-07-24T18:00:00+02:00', + end: '2026-07-26T14:00:00+02:00', + title: 'Gemeindewochenende', + href: 'https://www.link_to_event.com', + location: "St. Clara", + cancelled: false, + }, +} + export const EventInMarch: Story = { args: { date: '2024-03-24T15:00:00+01:00', diff --git a/src/components/EventRow/EventRow.tsx b/src/components/EventRow/EventRow.tsx index e4cd51d..93e4f8b 100644 --- a/src/components/EventRow/EventRow.tsx +++ b/src/components/EventRow/EventRow.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react' import styles from "./styles.module.scss" import classNames from 'classnames' import Link from 'next/link' +import { formatEventDate } from '@/utils/formatEventDate' export type EventRowProps = { /** datetime 8601 format */ @@ -41,8 +42,6 @@ const shortMonth = (date: string) => { return months[month - 1]; } - - export const EventRow = ({date, end, title, location, cancelled, href, color = "base", showDate = true}: EventRowProps) => { const day = useMemo(() => date.substring(8, 10), [date]); const dateObj = useMemo(() => new Date(date), [date]); @@ -80,10 +79,7 @@ export const EventRow = ({date, end, title, location, cancelled, href, color = " { showDate && <> - {dateObj.toLocaleDateString("de-DE", { weekday: "long" })} - {dayFormat === "long" && " " + dateObj.toLocaleDateString("de-DE", { dateStyle: "short" })}, {dateObj.toLocaleTimeString("de-DE", { timeStyle: "short", timeZone: "Europe/Berlin" })} - { endObj ? <> - {endObj.toLocaleTimeString("de-DE", { timeStyle: "short", timeZone: "Europe/Berlin" })} : "" } -  Uhr + {formatEventDate(dateObj, endObj, dayFormat)}
} diff --git a/src/utils/formatEventDate.test.ts b/src/utils/formatEventDate.test.ts new file mode 100644 index 0000000..80dd271 --- /dev/null +++ b/src/utils/formatEventDate.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from 'vitest' +import { formatEventDate } from './formatEventDate' + +// Fri 24.07.2026 18:00 Berlin time +const start = new Date('2026-07-24T18:00:00+02:00') +const endSameDay = new Date('2026-07-24T20:00:00+02:00') +const endNextDay = new Date('2026-07-25T01:00:00+02:00') +const endSunday = new Date('2026-07-26T14:00:00+02:00') + +describe('format "long"', () => { + test('start only', () => { + expect(formatEventDate(start)).toBe(`Freitag 24.07.26, 18:00 Uhr`) + }) + + test('same-day end renders as time range', () => { + expect(formatEventDate(start, endSameDay)).toBe(`Freitag 24.07.26, 18:00 - 20:00 Uhr`) + }) + + test('multi-day end renders both dates', () => { + expect(formatEventDate(start, endSunday)).toBe( + `Fr. 24.07. 18:00 Uhr - So. 26.07. 14:00 Uhr`, + ) + }) + + test('end after midnight counts as multi-day', () => { + expect(formatEventDate(start, endNextDay)).toBe( + `Fr. 24.07. 18:00 Uhr - Sa. 25.07. 01:00 Uhr`, + ) + }) +}) + +describe('format "short"', () => { + test('start only', () => { + expect(formatEventDate(start, undefined, 'short')).toBe(`Freitag, 18:00 Uhr`) + }) + + test('same-day end renders as time range without date', () => { + expect(formatEventDate(start, endSameDay, 'short')).toBe(`Freitag, 18:00 - 20:00 Uhr`) + }) + + test('multi-day end renders compact weekday range', () => { + expect(formatEventDate(start, endSunday, 'short')).toBe(`Fr 18:00 - So 14:00 Uhr`) + }) +}) + +describe('timezone handling', () => { + test('calendar day is compared in Europe/Berlin, not UTC', () => { + const lateStart = new Date('2026-07-24T22:30:00Z') + const earlyEnd = new Date('2026-07-25T01:00:00Z') + expect(formatEventDate(lateStart, earlyEnd)).toBe(`Samstag 25.07.26, 00:30 - 03:00 Uhr`) + }) +}) diff --git a/src/utils/formatEventDate.ts b/src/utils/formatEventDate.ts new file mode 100644 index 0000000..5e42ceb --- /dev/null +++ b/src/utils/formatEventDate.ts @@ -0,0 +1,39 @@ +/** + * Format an event's date and time range in a user-friendly way. + * + * format "long" (default): + * - same day: "Freitag 24.07.26, 18:00 - 20:00 Uhr" + * - multi-day: "Fr. 24.07. 18:00 Uhr - So. 26.07. 14:00 Uhr" + * + * format "short" (narrow screens): + * - same day: "Freitag, 18:00 - 20:00 Uhr" + * - multi-day: "Fr 18:00 - So 14:00 Uhr" + */ +export const formatEventDate = (start: Date, end?: Date, format: "long" | "short" = "long"): string => { + const time = (d: Date) => d.toLocaleTimeString("de-DE", { timeStyle: "short", timeZone: "Europe/Berlin" }) + const calendarDay = (d: Date) => d.toLocaleDateString("de-DE", { timeZone: "Europe/Berlin" }) + const weekday = (d: Date, length: "long" | "short") => + d.toLocaleDateString("de-DE", { weekday: length, timeZone: "Europe/Berlin" }) + const isMultiDay = end && calendarDay(start) !== calendarDay(end) + + switch (format) { + case "short": { + if (end && isMultiDay) { + return `${weekday(start, "short")} ${time(start)} - ${weekday(end, "short")} ${time(end)} Uhr` + } + const endTime = end ? ` - ${time(end)}` : "" + return `${weekday(start, "long")}, ${time(start)}${endTime} Uhr` + } + + case "long": { + if (end && isMultiDay) { + // "Fr. 24.07." — assembled manually because Intl puts a comma between weekday and date + const dayDate = (d: Date) => + `${weekday(d, "short")}. ${d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", timeZone: "Europe/Berlin" })}` + return `${dayDate(start)} ${time(start)} Uhr - ${dayDate(end)} ${time(end)} Uhr` + } + const endTime = end ? ` - ${time(end)}` : "" + return `${weekday(start, "long")} ${start.toLocaleDateString("de-DE", { dateStyle: "short", timeZone: "Europe/Berlin" })}, ${time(start)}${endTime} Uhr` + } + } +}