feat: replace Date constructor with parseISO for improved date handling across calendar components #25

This commit is contained in:
Linus Rath
2026-03-26 17:42:44 +01:00
parent 9b25b6d03e
commit 733e99f094
9 changed files with 22 additions and 18 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ export function CalendarAgendaView({
const grouped = useMemo(() => { const grouped = useMemo(() => {
const sorted = [...events].sort((a, b) => const sorted = [...events].sort((a, b) =>
new Date(a.start).getTime() - new Date(b.start).getTime() parseISO(a.start).getTime() - parseISO(b.start).getTime()
); );
const groups: DayGroup[] = []; const groups: DayGroup[] = [];
+3 -2
View File
@@ -121,13 +121,14 @@ export function CalendarMonthView({
try { try {
const data = JSON.parse(json); const data = JSON.parse(json);
const originalStart = parseISO(data.originalStart); const originalStart = parseISO(data.originalStart);
const event = useCalendarStore.getState().events.find(e => e.id === data.eventId);
const isAllDay = event?.showWithoutTime;
const newStart = new Date(day); const newStart = new Date(day);
newStart.setHours(originalStart.getHours(), originalStart.getMinutes(), originalStart.getSeconds(), 0); newStart.setHours(originalStart.getHours(), originalStart.getMinutes(), originalStart.getSeconds(), 0);
const newStartISO = format(newStart, "yyyy-MM-dd'T'HH:mm:ss"); const newStartISO = isAllDay ? format(newStart, "yyyy-MM-dd") : format(newStart, "yyyy-MM-dd'T'HH:mm:ss");
if (newStartISO === data.originalStart) return; if (newStartISO === data.originalStart) return;
const client = useAuthStore.getState().client; const client = useAuthStore.getState().client;
if (!client) return; if (!client) return;
const event = useCalendarStore.getState().events.find(e => e.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 {
+3 -3
View File
@@ -262,10 +262,10 @@ export function EventModal({
if (trimmedTitle.length > 500 || description.trim().length > 10000 || location.trim().length > 500) return; if (trimmedTitle.length > 500 || description.trim().length > 10000 || location.trim().length > 500) return;
const startStr = allDay const startStr = allDay
? `${startDate}T00:00:00` ? startDate
: `${startDate}T${startTime}:00`; : `${startDate}T${startTime}:00`;
const start = new Date(startStr); const start = allDay ? parseISO(startStr) : new Date(startStr);
let duration: string; let duration: string;
if (allDay) { if (allDay) {
@@ -393,7 +393,7 @@ export function EventModal({
uid: newUid, uid: newUid,
title: event.title, title: event.title,
description: event.description, description: event.description,
start: format(newStart, "yyyy-MM-dd'T'HH:mm:ss"), start: event.showWithoutTime ? format(newStart, "yyyy-MM-dd") : format(newStart, "yyyy-MM-dd'T'HH:mm:ss"),
duration: event.duration, duration: event.duration,
timeZone: event.timeZone, timeZone: event.timeZone,
showWithoutTime: event.showWithoutTime, showWithoutTime: event.showWithoutTime,
+2 -2
View File
@@ -7,7 +7,7 @@ import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
addMonths, subMonths, addYears, subYears, setMonth, setYear, addMonths, subMonths, addYears, subYears, setMonth, setYear,
eachDayOfInterval, getMonth, getYear, getISOWeek, getWeek, eachDayOfInterval, getMonth, getYear, getISOWeek, getWeek,
isSameDay, isSameMonth, isToday, format, isSameDay, isSameMonth, isToday, format, parseISO,
} from "date-fns"; } from "date-fns";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { CalendarEvent } from "@/lib/jmap/types"; import type { CalendarEvent } from "@/lib/jmap/types";
@@ -54,7 +54,7 @@ export function MiniCalendar({
const eventDates = useMemo(() => { const eventDates = useMemo(() => {
const set = new Set<string>(); const set = new Set<string>();
events.forEach(e => { events.forEach(e => {
try { set.add(format(new Date(e.start), "yyyy-MM-dd")); } catch { /* skip */ } try { set.add(format(parseISO(e.start), "yyyy-MM-dd")); } catch { /* skip */ }
}); });
return set; return set;
}, [events]); }, [events]);
+1 -1
View File
@@ -97,7 +97,7 @@ export function TaskModal({
due = `${dueDate}T${dueTime}:00`; due = `${dueDate}T${dueTime}:00`;
showWithoutTime = false; showWithoutTime = false;
} else { } else {
due = `${dueDate}T00:00:00`; due = dueDate;
showWithoutTime = true; showWithoutTime = true;
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@
import { useEffect, useRef, useCallback } from 'react'; import { useEffect, useRef, useCallback } from 'react';
import { useTranslations, useLocale } from 'next-intl'; import { useTranslations, useLocale } from 'next-intl';
import { parseISO } from 'date-fns';
import { useAuthStore } from '@/stores/auth-store'; import { useAuthStore } from '@/stores/auth-store';
import { useCalendarStore } from '@/stores/calendar-store'; import { useCalendarStore } from '@/stores/calendar-store';
import { useSettingsStore } from '@/stores/settings-store'; import { useSettingsStore } from '@/stores/settings-store';
@@ -50,7 +51,7 @@ export function useCalendarAlerts() {
playNotificationSound(notificationSoundChoice); playNotificationSound(notificationSoundChoice);
} }
const diffMs = new Date(alert.event.utcStart || alert.event.start).getTime() - now; const diffMs = (alert.event.utcStart ? new Date(alert.event.utcStart).getTime() : parseISO(alert.event.start).getTime()) - now;
const diffMin = Math.round(diffMs / 60000); const diffMin = Math.round(diffMs / 60000);
const timeLabel = diffMin <= 0 const timeLabel = diffMin <= 0
+3 -2
View File
@@ -1,3 +1,4 @@
import { parseISO } from 'date-fns';
import type { import type {
CalendarEvent, CalendarEvent,
CalendarEventAlert, CalendarEventAlert,
@@ -53,7 +54,7 @@ export function computeFireTime(
baseTime = new Date(event.utcEnd).getTime(); baseTime = new Date(event.utcEnd).getTime();
} else { } else {
// Compute end from start + duration // Compute end from start + duration
const startMs = new Date(event.start).getTime(); const startMs = parseISO(event.start).getTime();
if (Number.isNaN(startMs)) return null; if (Number.isNaN(startMs)) return null;
const durationMin = parseDuration(event.duration); const durationMin = parseDuration(event.duration);
baseTime = startMs + durationMin * 60000; baseTime = startMs + durationMin * 60000;
@@ -61,7 +62,7 @@ export function computeFireTime(
} else { } else {
baseTime = event.utcStart baseTime = event.utcStart
? new Date(event.utcStart).getTime() ? new Date(event.utcStart).getTime()
: new Date(event.start).getTime(); : parseISO(event.start).getTime();
} }
if (Number.isNaN(baseTime)) return null; if (Number.isNaN(baseTime)) return null;
+2 -1
View File
@@ -1,3 +1,4 @@
import { parseISO } from 'date-fns';
import type { Email, Attachment, CalendarEvent, CalendarParticipant, EmailBodyPart } from '@/lib/jmap/types'; import type { Email, Attachment, CalendarEvent, CalendarParticipant, EmailBodyPart } from '@/lib/jmap/types';
export type InvitationMethod = export type InvitationMethod =
@@ -534,7 +535,7 @@ function addDurationToDate(start: string, duration: string, _timeZone?: string |
const minutes = parseInt(match[4] || '0'); const minutes = parseInt(match[4] || '0');
const seconds = parseInt(match[5] || '0'); const seconds = parseInt(match[5] || '0');
const date = new Date(start); const date = parseISO(start);
if (isNaN(date.getTime())) return null; if (isNaN(date.getTime())) return null;
const isUTC = start.endsWith('Z') || start.includes('+'); const isUTC = start.endsWith('Z') || start.includes('+');
+5 -5
View File
@@ -12,14 +12,14 @@ export interface CalendarWeekSegment {
} }
export function getEventEndDate(event: CalendarEvent): Date { export function getEventEndDate(event: CalendarEvent): Date {
const start = new Date(event.start); const start = parseISO(event.start);
if (!event.duration) return start; if (!event.duration) return start;
return new Date(start.getTime() + parseDuration(event.duration) * 60000); return new Date(start.getTime() + parseDuration(event.duration) * 60000);
} }
export function getEventDisplayEndDate(event: CalendarEvent): Date { export function getEventDisplayEndDate(event: CalendarEvent): Date {
const end = getEventEndDate(event); const end = getEventEndDate(event);
if (!event.showWithoutTime || end.getTime() <= new Date(event.start).getTime()) { if (!event.showWithoutTime || end.getTime() <= parseISO(event.start).getTime()) {
return end; return end;
} }
return subMilliseconds(end, 1); return subMilliseconds(end, 1);
@@ -27,7 +27,7 @@ export function getEventDisplayEndDate(event: CalendarEvent): Date {
export function getEventDayBounds(event: CalendarEvent): { startDay: Date; endDay: Date } { export function getEventDayBounds(event: CalendarEvent): { startDay: Date; endDay: Date } {
return { return {
startDay: startOfDay(new Date(event.start)), startDay: startOfDay(parseISO(event.start)),
endDay: startOfDay(getEventDisplayEndDate(event)), endDay: startOfDay(getEventDisplayEndDate(event)),
}; };
} }
@@ -77,7 +77,7 @@ export function buildWeekSegments(events: CalendarEvent[], weekDays: Date[]): Ca
if (left.event.showWithoutTime !== right.event.showWithoutTime) { if (left.event.showWithoutTime !== right.event.showWithoutTime) {
return left.event.showWithoutTime ? -1 : 1; return left.event.showWithoutTime ? -1 : 1;
} }
const timeDiff = new Date(left.event.start).getTime() - new Date(right.event.start).getTime(); const timeDiff = parseISO(left.event.start).getTime() - parseISO(right.event.start).getTime();
if (timeDiff !== 0) return timeDiff; if (timeDiff !== 0) return timeDiff;
return (left.event.title || "").localeCompare(right.event.title || ""); return (left.event.title || "").localeCompare(right.event.title || "");
}); });
@@ -100,7 +100,7 @@ export function layoutOverlappingEvents(
events: CalendarEvent[], events: CalendarEvent[],
): { event: CalendarEvent; column: number; totalColumns: number }[] { ): { event: CalendarEvent; column: number; totalColumns: number }[] {
const sorted = [...events].sort((a, b) => { const sorted = [...events].sort((a, b) => {
const diff = new Date(a.start).getTime() - new Date(b.start).getTime(); const diff = parseISO(a.start).getTime() - parseISO(b.start).getTime();
if (diff !== 0) return diff; if (diff !== 0) return diff;
return parseDuration(b.duration) - parseDuration(a.duration); return parseDuration(b.duration) - parseDuration(a.duration);
}); });