diff --git a/hooks/use-time-grid-interactions.ts b/hooks/use-time-grid-interactions.ts index 5b5e1355..e4d992ba 100644 --- a/hooks/use-time-grid-interactions.ts +++ b/hooks/use-time-grid-interactions.ts @@ -4,6 +4,7 @@ import { useAuthStore } from "@/stores/auth-store"; import { useCalendarStore } from "@/stores/calendar-store"; import { toast } from "@/stores/toast-store"; import { debug } from "@/lib/debug"; +import { formatIsoInTimeZone } from "@/lib/calendar-utils"; import type { Calendar } from "@/lib/jmap/types"; interface DragCreateState { @@ -320,14 +321,18 @@ export function useTimeGridInteractions({ const minutes = snapDragMinutes(e); const newStart = new Date(day); newStart.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0); - const newStartISO = format(newStart, "yyyy-MM-dd'T'HH:mm:ss"); + const event = useCalendarStore.getState().events.find(ev => ev.id === data.eventId); + // Emit `start` as a floating wall-clock in the event's own timeZone. + // If we used browser-local, the server would reinterpret it in event.timeZone + // and shift the event by the offset between the two zones. + const eventTimeZone = event?.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone; + const newStartISO = formatIsoInTimeZone(newStart, eventTimeZone); if (newStartISO === data.originalStart) return; const client = useAuthStore.getState().client; if (!client) { toast.error(errorMessages.move); return; } - const event = useCalendarStore.getState().events.find(ev => ev.id === data.eventId); const hasParticipants = event?.participants && Object.keys(event.participants).length > 0; await useCalendarStore.getState().updateEvent(client, data.eventId, { start: newStartISO }, hasParticipants || undefined); } catch { diff --git a/lib/calendar-utils.ts b/lib/calendar-utils.ts index 67b7820c..64179fb3 100644 --- a/lib/calendar-utils.ts +++ b/lib/calendar-utils.ts @@ -272,3 +272,22 @@ export function formatSnapTime(minutes: number, timeFormat: "12h" | "24h"): stri export function getPrimaryCalendarId(event: Pick): string | undefined { return Object.keys(event.calendarIds || {})[0]; } + +export function formatIsoInTimeZone(date: Date, timeZone: string): string { + const parts = new Intl.DateTimeFormat("en-CA", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).formatToParts(date); + const map: Record = {}; + for (const part of parts) { + if (part.type !== "literal") map[part.type] = part.value; + } + const hour = map.hour === "24" ? "00" : map.hour; + return `${map.year}-${map.month}-${map.day}T${hour}:${map.minute}:${map.second}`; +} diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts index 888f6a00..7d7125bb 100644 --- a/stores/calendar-store.ts +++ b/stores/calendar-store.ts @@ -4,6 +4,7 @@ import type { IJMAPClient } from '@/lib/jmap/client-interface'; import type { Calendar, CalendarEvent, CalendarParticipant } from '@/lib/jmap/types'; import { debug } from '@/lib/debug'; import { normalizeAllDayDuration } from '@/lib/calendar-utils'; +import { parseDuration } from '@/components/calendar/event-card'; import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization'; import { expandRecurringEvents } from '@/lib/recurrence-expansion'; import { generateUUID } from '@/lib/utils'; @@ -347,6 +348,14 @@ export const useCalendarStore = create()( } } } + // When duration changes (e.g. resize), recompute utcEnd so the event + // renders with the new length immediately without waiting for refresh. + if (cleanUpdates.duration !== undefined && merged.utcStart) { + const durationMinutes = parseDuration(cleanUpdates.duration); + merged.utcEnd = new Date( + new Date(merged.utcStart).getTime() + durationMinutes * 60000, + ).toISOString(); + } return merged; }), }));