feat: add timezone-aware formatting for event start times and update utcEnd on duration change
This commit is contained in:
@@ -4,6 +4,7 @@ import { useAuthStore } from "@/stores/auth-store";
|
|||||||
import { useCalendarStore } from "@/stores/calendar-store";
|
import { useCalendarStore } from "@/stores/calendar-store";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
import { debug } from "@/lib/debug";
|
import { debug } from "@/lib/debug";
|
||||||
|
import { formatIsoInTimeZone } from "@/lib/calendar-utils";
|
||||||
import type { Calendar } from "@/lib/jmap/types";
|
import type { Calendar } from "@/lib/jmap/types";
|
||||||
|
|
||||||
interface DragCreateState {
|
interface DragCreateState {
|
||||||
@@ -320,14 +321,18 @@ export function useTimeGridInteractions({
|
|||||||
const minutes = snapDragMinutes(e);
|
const minutes = snapDragMinutes(e);
|
||||||
const newStart = new Date(day);
|
const newStart = new Date(day);
|
||||||
newStart.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
|
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;
|
if (newStartISO === data.originalStart) return;
|
||||||
const client = useAuthStore.getState().client;
|
const client = useAuthStore.getState().client;
|
||||||
if (!client) {
|
if (!client) {
|
||||||
toast.error(errorMessages.move);
|
toast.error(errorMessages.move);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const event = useCalendarStore.getState().events.find(ev => ev.id === data.eventId);
|
|
||||||
const hasParticipants = event?.participants && Object.keys(event.participants).length > 0;
|
const hasParticipants = event?.participants && Object.keys(event.participants).length > 0;
|
||||||
await useCalendarStore.getState().updateEvent(client, data.eventId, { start: newStartISO }, hasParticipants || undefined);
|
await useCalendarStore.getState().updateEvent(client, data.eventId, { start: newStartISO }, hasParticipants || undefined);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -272,3 +272,22 @@ export function formatSnapTime(minutes: number, timeFormat: "12h" | "24h"): stri
|
|||||||
export function getPrimaryCalendarId(event: Pick<CalendarEvent, 'calendarIds'>): string | undefined {
|
export function getPrimaryCalendarId(event: Pick<CalendarEvent, 'calendarIds'>): string | undefined {
|
||||||
return Object.keys(event.calendarIds || {})[0];
|
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<string, string> = {};
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|||||||
import type { Calendar, CalendarEvent, CalendarParticipant } from '@/lib/jmap/types';
|
import type { Calendar, CalendarEvent, CalendarParticipant } from '@/lib/jmap/types';
|
||||||
import { debug } from '@/lib/debug';
|
import { debug } from '@/lib/debug';
|
||||||
import { normalizeAllDayDuration } from '@/lib/calendar-utils';
|
import { normalizeAllDayDuration } from '@/lib/calendar-utils';
|
||||||
|
import { parseDuration } from '@/components/calendar/event-card';
|
||||||
import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization';
|
import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization';
|
||||||
import { expandRecurringEvents } from '@/lib/recurrence-expansion';
|
import { expandRecurringEvents } from '@/lib/recurrence-expansion';
|
||||||
import { generateUUID } from '@/lib/utils';
|
import { generateUUID } from '@/lib/utils';
|
||||||
@@ -347,6 +348,14 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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;
|
return merged;
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user