fix: recurring event DST time shift and block height overflow
- Use TZID-based local time (instead of UTC 'Z') for DTSTART/DTEND when creating recurring CalDAV events, preventing DST-related time shifts (e.g. 13:40 CET showing as 14:40 CEST after clock change) - Send browser timezone from CalendarEventModal to server - Guard against NaN event duration (missing/invalid endTime) which caused event blocks to stretch to end of day v1.61.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c5dc315f82
commit
1011aef639
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.61.0",
|
||||
"version": "1.61.1",
|
||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -40,7 +40,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const body = await request.json();
|
||||
const { calendarId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, url,
|
||||
reminders, busyStatus, visibility, attendees, attachments } = body;
|
||||
reminders, busyStatus, visibility, attendees, attachments, timezone } = body;
|
||||
|
||||
console.log('[API] Creating event:', { calendarId, title, start, end });
|
||||
|
||||
@ -67,6 +67,7 @@ export async function POST(request: NextRequest) {
|
||||
recurrenceCount,
|
||||
recurrenceInterval,
|
||||
recurrenceDays,
|
||||
timezone,
|
||||
url,
|
||||
reminders,
|
||||
busyStatus,
|
||||
|
||||
@ -194,6 +194,7 @@ export default function CalendarEventModal({
|
||||
allDay,
|
||||
start: { dateTime: startDate.toISOString() },
|
||||
end: { dateTime: endDate.toISOString() },
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
reminders: activeReminders.length > 0 ? activeReminders : undefined,
|
||||
busyStatus: busyStatus !== 'busy' ? busyStatus : undefined,
|
||||
visibility: visibility !== 'default' ? visibility : undefined,
|
||||
|
||||
@ -3546,6 +3546,11 @@ export default function WeeklyView() {
|
||||
const end = new Date(event.endTime);
|
||||
const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60);
|
||||
|
||||
// Guard against NaN or negative durations (missing/invalid end time)
|
||||
if (!isFinite(durationMinutes) || durationMinutes <= 0) {
|
||||
return getSlotHeight(effectiveCellDuration); // Default to one slot height
|
||||
}
|
||||
|
||||
// Calculate height based on duration and slot height
|
||||
const pixelsPerMinute = getSlotHeight(effectiveCellDuration) / effectiveCellDuration;
|
||||
return Math.max(
|
||||
|
||||
@ -394,6 +394,7 @@ export const createEvent = async (
|
||||
recurrenceCount?: number;
|
||||
recurrenceInterval?: number;
|
||||
recurrenceDays?: number[];
|
||||
timezone?: string;
|
||||
start: { dateTime?: string; date?: string };
|
||||
end: { dateTime?: string; date?: string };
|
||||
reminders?: Array<{ method: string; minutes: number }>;
|
||||
@ -439,12 +440,30 @@ export const createEvent = async (
|
||||
// For all-day events, end date is exclusive, so if they are same, add 1 day
|
||||
// But typically UI handles this. Let's assume input is correct.
|
||||
} else if (eventData.start.dateTime) {
|
||||
// Timed event
|
||||
// For recurring events, use local time with TZID to avoid DST shifts
|
||||
if (eventData.recurrence && eventData.timezone) {
|
||||
const tz = eventData.timezone;
|
||||
const toLocalIcal = (isoStr: string) => {
|
||||
const d = new Date(isoStr);
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||
}).formatToParts(d);
|
||||
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
|
||||
return `${get('year')}${get('month')}${get('day')}T${get('hour')}${get('minute')}${get('second')}`;
|
||||
};
|
||||
dtStart = toLocalIcal(eventData.start.dateTime);
|
||||
dtEnd = eventData.end.dateTime ? toLocalIcal(eventData.end.dateTime) : dtStart;
|
||||
dtStartParam = `;TZID=${tz}`;
|
||||
dtEndParam = `;TZID=${tz}`;
|
||||
} else {
|
||||
// Timed event (non-recurring or no timezone info)
|
||||
dtStart = new Date(eventData.start.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';
|
||||
dtEnd = eventData.end.dateTime
|
||||
? new Date(eventData.end.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z'
|
||||
: dtStart;
|
||||
}
|
||||
}
|
||||
|
||||
const description = eventData.description ? `DESCRIPTION:${eventData.description.replace(/\n/g, '\\n')}\r\n` : '';
|
||||
const location = eventData.location ? `LOCATION:${eventData.location.replace(/,/g, '\\,')}\r\n` : '';
|
||||
|
||||
@ -45,6 +45,7 @@ export interface CalendarEvent {
|
||||
recurrenceCount?: number;
|
||||
recurrenceInterval?: number;
|
||||
recurrenceDays?: number[];
|
||||
timezone?: string;
|
||||
recurringEventId?: string;
|
||||
isRecurring?: boolean;
|
||||
source: 'google' | 'apple' | 'outlook' | 'synology' | 'notion';
|
||||
@ -952,6 +953,7 @@ export const createCalendarEvent = async (
|
||||
recurrenceCount: event.recurrenceCount,
|
||||
recurrenceInterval: event.recurrenceInterval,
|
||||
recurrenceDays: event.recurrenceDays,
|
||||
timezone: event.timezone,
|
||||
start,
|
||||
end,
|
||||
reminders: event.reminders,
|
||||
@ -992,6 +994,7 @@ export const createCalendarEvent = async (
|
||||
recurrenceCount: event.recurrenceCount,
|
||||
recurrenceInterval: event.recurrenceInterval,
|
||||
recurrenceDays: event.recurrenceDays,
|
||||
timezone: event.timezone,
|
||||
start: event.start!,
|
||||
end: event.end!,
|
||||
reminders: event.reminders,
|
||||
|
||||
@ -404,6 +404,7 @@ export const createEvent = async (
|
||||
recurrenceCount?: number;
|
||||
recurrenceInterval?: number;
|
||||
recurrenceDays?: number[];
|
||||
timezone?: string;
|
||||
start: { dateTime?: string; date?: string };
|
||||
end: { dateTime?: string; date?: string };
|
||||
reminders?: Array<{ method: string; minutes: number }>;
|
||||
@ -440,11 +441,29 @@ export const createEvent = async (
|
||||
dtStartParam = ';VALUE=DATE';
|
||||
dtEndParam = ';VALUE=DATE';
|
||||
} else if (eventData.start.dateTime) {
|
||||
// For recurring events, use local time with TZID to avoid DST shifts
|
||||
if (eventData.recurrence && eventData.timezone) {
|
||||
const tz = eventData.timezone;
|
||||
const toLocalIcal = (isoStr: string) => {
|
||||
const d = new Date(isoStr);
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||
}).formatToParts(d);
|
||||
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
|
||||
return `${get('year')}${get('month')}${get('day')}T${get('hour')}${get('minute')}${get('second')}`;
|
||||
};
|
||||
dtStart = toLocalIcal(eventData.start.dateTime);
|
||||
dtEnd = eventData.end.dateTime ? toLocalIcal(eventData.end.dateTime) : dtStart;
|
||||
dtStartParam = `;TZID=${tz}`;
|
||||
dtEndParam = `;TZID=${tz}`;
|
||||
} else {
|
||||
dtStart = new Date(eventData.start.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';
|
||||
dtEnd = eventData.end.dateTime
|
||||
? new Date(eventData.end.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z'
|
||||
: dtStart;
|
||||
}
|
||||
}
|
||||
|
||||
const description = eventData.description ? `DESCRIPTION:${eventData.description.replace(/\n/g, '\\n')}\r\n` : '';
|
||||
const location = eventData.location ? `LOCATION:${eventData.location.replace(/,/g, '\\,')}\r\n` : '';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user