feat: use friendly event date labels

This commit is contained in:
2026-04-09 17:41:37 -04:00
parent 12f2fd95dc
commit e01a7ed1ad
4 changed files with 94 additions and 31 deletions

View File

@@ -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 (
<motion.div
layout
@@ -66,15 +50,13 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
exit={{ opacity: 0, y: -8, transition: { duration: 0.15 } }}
transition={{ duration: 0.2 }}
>
<div className="glass-card p-4 group cursor-pointer hover:bg-accent/50 transition-colors duration-150">
<div className="glass-card group cursor-pointer p-4 transition-colors duration-150 hover:bg-accent/50">
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0 space-y-1.5">
<h3 className="font-medium text-sm leading-snug truncate">
{event.title}
</h3>
<div className="min-w-0 flex-1 space-y-1.5">
<h3 className="truncate text-sm font-medium leading-snug">{event.title}</h3>
{event.description && (
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
<p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground">
{event.description}
</p>
)}
@@ -82,9 +64,7 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
<Clock className="h-3 w-3 shrink-0" />
{formatDateTime(event.start, event.allDay)}
{endDate && <span className="text-muted-foreground/50">-</span>}
{endDate}
{formatEventRangeLabel(event)}
</span>
{event.location && (
@@ -98,24 +78,24 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
<Button
variant="link"
size="sm"
className="gap-1 h-auto p-0 text-xs text-primary/70 hover:text-primary"
className="h-auto gap-1 p-0 text-xs text-primary/70 hover:text-primary"
asChild
>
<a
href={event.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
onClick={(currentEvent) => currentEvent.stopPropagation()}
>
<ExternalLink className="h-3 w-3" />
<span className="truncate max-w-[120px]">Link</span>
<span className="max-w-[120px] truncate">Link</span>
</a>
</Button>
)}
</div>
{event.recurrenceRule && (
<RRuleDisplay rrule={event.recurrenceRule} />
<RRuleDisplay rrule={event.recurrenceRule} start={event.start} />
)}
</div>

View File

@@ -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<CalendarEvent, "start" | "end" | "allDay">): 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")}`;
};

View File

@@ -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:0011:00");
expect(markup).not.toContain("AM");
});
});

View File

@@ -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:0011: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");
});
});