diff --git a/src/components/event-card.tsx b/src/components/event-card.tsx index 3447eb9..df662a1 100644 --- a/src/components/event-card.tsx +++ b/src/components/event-card.tsx @@ -18,6 +18,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { formatEventRangeLabel } from "@/lib/event-date-format"; import type { CalendarEvent } from "@/lib/types"; interface EventCardProps { @@ -27,21 +28,6 @@ interface EventCardProps { } export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => { - const formatDateTime = (dateStr: string, allDay: boolean | undefined) => { - return allDay - ? new Date(dateStr).toLocaleDateString(undefined, { - month: "short", - day: "numeric", - year: "numeric", - }) - : new Date(dateStr).toLocaleString(undefined, { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - }); - }; - const handleEdit = () => { onEdit({ id: event.id, @@ -52,12 +38,10 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => { start: event.start, end: event.end || "", allDay: event.allDay || false, + recurrenceRule: event.recurrenceRule, }); }; - const endDate = - event.end && !event.allDay ? formatDateTime(event.end, event.allDay) : null; - return ( { exit={{ opacity: 0, y: -8, transition: { duration: 0.15 } }} transition={{ duration: 0.2 }} > -
+
-
-

- {event.title} -

+
+

{event.title}

{event.description && ( -

+

{event.description}

)} @@ -82,9 +64,7 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
- {formatDateTime(event.start, event.allDay)} - {endDate && -} - {endDate} + {formatEventRangeLabel(event)} {event.location && ( @@ -98,24 +78,24 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => { )}
{event.recurrenceRule && ( - + )}
diff --git a/src/lib/event-date-format.ts b/src/lib/event-date-format.ts new file mode 100644 index 0000000..98280fc --- /dev/null +++ b/src/lib/event-date-format.ts @@ -0,0 +1,38 @@ +import { + format, + isSameDay, + isToday, + isTomorrow, + parseISO, +} from "date-fns"; +import type { CalendarEvent } from "@/lib/types"; + +const getFriendlyDayLabel = (value: Date): string => { + if (isToday(value)) return "Today"; + if (isTomorrow(value)) return "Tomorrow"; + return format(value, "MMM d, yyyy"); +}; + +export const formatEventStartLabel = (start: string, allDay?: boolean): string => { + const parsed = parseISO(start); + const dayLabel = getFriendlyDayLabel(parsed); + + if (allDay) return dayLabel; + return `${dayLabel} · ${format(parsed, "HH:mm")}`; +}; + +export const formatEventRangeLabel = (event: Pick): string => { + const startDate = parseISO(event.start); + const startLabel = getFriendlyDayLabel(startDate); + + if (event.allDay || !event.end) { + return event.allDay ? startLabel : `${startLabel} · ${format(startDate, "HH:mm")}`; + } + + const endDate = parseISO(event.end); + if (isSameDay(startDate, endDate)) { + return `${startLabel} · ${format(startDate, "HH:mm")}–${format(endDate, "HH:mm")}`; + } + + return `${startLabel} · ${format(startDate, "HH:mm")} → ${getFriendlyDayLabel(endDate)} · ${format(endDate, "HH:mm")}`; +}; diff --git a/tests/event-card.test.ts b/tests/event-card.test.ts index 7afc985..382dee2 100644 --- a/tests/event-card.test.ts +++ b/tests/event-card.test.ts @@ -28,4 +28,17 @@ describe("EventCard actions trigger", () => { expect(markup).not.toContain("opacity-0"); expect(markup).not.toContain("group-hover:opacity-100"); }); + + test("renders friendly shared date formatting instead of native locale strings", () => { + const markup = renderToStaticMarkup( + EventCard({ + event: sampleEvent, + onEdit: () => {}, + onDelete: () => {}, + }), + ); + + expect(markup).toContain("10:00–11:00"); + expect(markup).not.toContain("AM"); + }); }); diff --git a/tests/event-date-format.test.ts b/tests/event-date-format.test.ts new file mode 100644 index 0000000..4cb6a2e --- /dev/null +++ b/tests/event-date-format.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { addDays } from "date-fns"; +import { formatEventRangeLabel, formatEventStartLabel } from "@/lib/event-date-format"; + +describe("event-date-format", () => { + test("formats a timed start-only event with a friendly day label", () => { + const today = new Date(); + today.setHours(10, 0, 0, 0); + + expect(formatEventStartLabel(today.toISOString(), false)).toContain("Today · 10:00"); + }); + + test("formats a same-day timed range without native locale helpers", () => { + const today = new Date(); + today.setHours(10, 0, 0, 0); + const end = new Date(today); + end.setHours(11, 30, 0, 0); + + expect( + formatEventRangeLabel({ start: today.toISOString(), end: end.toISOString(), allDay: false }), + ).toContain("10:00–11:30"); + }); + + test("formats all-day events with tomorrow labels when applicable", () => { + const tomorrow = addDays(new Date(), 1); + tomorrow.setHours(0, 0, 0, 0); + + expect( + formatEventRangeLabel({ start: tomorrow.toISOString(), end: undefined, allDay: true }), + ).toBe("Tomorrow"); + }); +});