fix: use UTC timestamps for timed event rendering

This commit is contained in:
Linus Rath
2026-03-28 00:33:38 +01:00
parent 8eeabfc995
commit af8ea8349e
9 changed files with 75 additions and 34 deletions
+5 -5
View File
@@ -2,11 +2,11 @@
import { useMemo, useRef, useEffect, useCallback } from "react";
import { useTranslations, useFormatter } from "next-intl";
import { format, parseISO, isToday, isTomorrow, startOfDay } from "date-fns";
import { format, isToday, isTomorrow, startOfDay } from "date-fns";
import { MapPin, Users } from "lucide-react";
import { cn } from "@/lib/utils";
import { parseDuration, getEventColor } from "./event-card";
import { getEventDayBounds, getPrimaryCalendarId } from "@/lib/calendar-utils";
import { getEventDayBounds, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils";
import { getParticipantCount } from "@/lib/calendar-participants";
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
@@ -49,7 +49,7 @@ export function CalendarAgendaView({
const grouped = useMemo(() => {
const sorted = [...events].sort((a, b) =>
parseISO(a.start).getTime() - parseISO(b.start).getTime()
getEventStartDate(a).getTime() - getEventStartDate(b).getTime()
);
const groups: DayGroup[] = [];
@@ -144,9 +144,9 @@ export function CalendarAgendaView({
const calId = getPrimaryCalendarId(ev);
const calendar = calId ? calendarMap.get(calId) : undefined;
const color = getEventColor(ev, calendar);
const start = parseISO(ev.start);
const start = getEventStartDate(ev);
const durMin = parseDuration(ev.duration);
const end = new Date(start.getTime() + durMin * 60000);
const end = getEventEndDate(ev);
const locationName = ev.locations
? Object.values(ev.locations)[0]?.name
: null;
+4 -3
View File
@@ -4,9 +4,10 @@ import { useCallback, useState, type CSSProperties, type DragEvent } from "react
import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
import { format, parseISO } from "date-fns";
import { format } from "date-fns";
import { Users } from "lucide-react";
import { getParticipantCount } from "@/lib/calendar-participants";
import { getEventEndDate, getEventStartDate } from "@/lib/calendar-utils";
import { useSettingsStore } from "@/stores/settings-store";
interface EventCardProps {
@@ -68,14 +69,14 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
const t = useTranslations("calendar");
const [isBeingDragged, setIsBeingDragged] = useState(false);
const color = getEventColor(event, calendar);
const startDate = parseISO(event.start);
const startDate = getEventStartDate(event);
const timeFormat = useSettingsStore((state) => state.timeFormat);
const showTimeInMonthView = useSettingsStore((state) => state.showTimeInMonthView);
const timeFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
const calendarName = calendar?.name || "";
const durationMinutes = parseDuration(event.duration);
const endTime = new Date(startDate.getTime() + durationMinutes * 60000);
const endTime = getEventEndDate(event);
const timeString = `${format(startDate, timeFmt)} ${format(endTime, timeFmt)}`;
const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`;
+3 -2
View File
@@ -12,6 +12,7 @@ import { format, parseISO } from "date-fns";
import { cn } from "@/lib/utils";
import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types";
import { parseDuration, getEventColor } from "./event-card";
import { getEventEndDate, getEventStartDate } from "@/lib/calendar-utils";
import {
isOrganizer,
getUserParticipantId,
@@ -138,9 +139,9 @@ export function EventDetailPopover({
const [isSavingNote, setIsSavingNote] = useState(false);
const color = getEventColor(event, calendar);
const startDate = parseISO(event.start);
const startDate = getEventStartDate(event);
const durationMinutes = parseDuration(event.duration);
const endDate = new Date(startDate.getTime() + durationMinutes * 60000);
const endDate = getEventEndDate(event);
const locationName = useMemo(() => {
if (!event.locations) return null;
+8 -10
View File
@@ -8,7 +8,7 @@ import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Vid
import { format, parseISO, addHours, addDays } from "date-fns";
import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types";
import { parseDuration, getEventColor } from "./event-card";
import { buildAllDayDuration, getEventDisplayEndDate, getPrimaryCalendarId } from "@/lib/calendar-utils";
import { buildAllDayDuration, getEventDisplayEndDate, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils";
import { ParticipantInput } from "./participant-input";
import {
isOrganizer,
@@ -159,7 +159,7 @@ export function EventModal({
}, [event, existingParticipants]);
const getInitialStart = (): Date => {
if (event?.start) return parseISO(event.start);
if (event?.start) return getEventStartDate(event);
if (defaultDate) {
const d = new Date(defaultDate);
if (defaultEndDate) return d;
@@ -177,9 +177,7 @@ export function EventModal({
if (event.showWithoutTime) {
return getEventDisplayEndDate(event);
}
const s = parseISO(event.start);
const dur = parseDuration(event.duration);
return new Date(s.getTime() + dur * 60000);
return getEventEndDate(event);
}
if (defaultEndDate) return new Date(defaultEndDate);
return addHours(getInitialStart(), 1);
@@ -384,7 +382,7 @@ export function EventModal({
const handleDuplicate = useCallback(() => {
if (!event || !onDuplicate) return;
const start = parseISO(event.start);
const start = getEventStartDate(event);
const newStart = addDays(start, 1);
const newUid = typeof crypto !== 'undefined' && crypto.randomUUID
? crypto.randomUUID()
@@ -456,9 +454,9 @@ export function EventModal({
const hasParticipants = attendees.length > 0 || (event?.participants && Object.keys(event.participants).length > 0);
if (isAttendeeMode && event) {
const startD = parseISO(event.start);
const startD = getEventStartDate(event);
const durMin = parseDuration(event.duration);
const endD = new Date(startD.getTime() + durMin * 60000);
const endD = getEventEndDate(event);
const locationName = event.locations ? Object.values(event.locations)[0]?.name : null;
const participants = getParticipantList(event);
@@ -567,9 +565,9 @@ export function EventModal({
// View mode: read-only display of event details with Edit button
if (mode === "view" && event) {
const startD = parseISO(event.start);
const startD = getEventStartDate(event);
const durMin = parseDuration(event.duration);
const endD = new Date(startD.getTime() + durMin * 60000);
const endD = getEventEndDate(event);
const locationName = event.locations ? Object.values(event.locations)[0]?.name || null : null;
const virtualLoc = event.virtualLocations ? Object.values(event.virtualLocations)[0]?.uri || null : null;
const viewParticipants = getParticipantList(event);
+2 -1
View File
@@ -7,6 +7,7 @@ import { X, Upload, Check, Loader2, RefreshCw, Globe } from "lucide-react";
import { format, parseISO } from "date-fns";
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
import type { IJMAPClient } from '@/lib/jmap/client-interface';
import { getEventStartDate } from "@/lib/calendar-utils";
import { useCalendarStore } from "@/stores/calendar-store";
import { useSettingsStore } from "@/stores/settings-store";
import { toast } from "@/stores/toast-store";
@@ -195,7 +196,7 @@ export function ICalImportModal({ calendars, client, onClose }: ICalImportModalP
const formatEventDate = (event: Partial<CalendarEvent>): string => {
if (!event.start) return "";
try {
const date = parseISO(event.start);
const date = getEventStartDate(event as CalendarEvent);
const timeFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
return event.showWithoutTime
? format(date, "MMM d, yyyy")
+10 -2
View File
@@ -7,9 +7,10 @@ import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
addMonths, subMonths, addYears, subYears, setMonth, setYear,
eachDayOfInterval, getMonth, getYear, getISOWeek, getWeek,
isSameDay, isSameMonth, isToday, format, parseISO,
isSameDay, isSameMonth, isToday, format,
} from "date-fns";
import { cn } from "@/lib/utils";
import { getEventDayBounds } from "@/lib/calendar-utils";
import type { CalendarEvent } from "@/lib/jmap/types";
type PickerView = "days" | "months" | "years";
@@ -54,7 +55,14 @@ export function MiniCalendar({
const eventDates = useMemo(() => {
const set = new Set<string>();
events.forEach(e => {
try { set.add(format(parseISO(e.start), "yyyy-MM-dd")); } catch { /* skip */ }
try {
const { startDay, endDay } = getEventDayBounds(e);
const cursor = new Date(startDay);
while (cursor <= endDay) {
set.add(format(cursor, "yyyy-MM-dd"));
cursor.setDate(cursor.getDate() + 1);
}
} catch { /* skip */ }
});
return set;
}, [events]);