diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b58bd..96c0011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project are documented in this file. +## [1.116.3] - 2026-09-05 + +### Added +- Calendar events can now be reassigned to another editable calendar from the event editor. +- Moving an event updates the source and target calendar caches. + +### Changed +- Calendar event backgrounds now use the calendar color with a lighter, readable contrast. +- Google event-specific colors no longer override the selected calendar's color. + ## [1.116.2] - 2026-09-05 ### Fixed diff --git a/package-lock.json b/package-lock.json index 86adcce..aa3bb03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "my-weekly-todo-list", - "version": "1.116.2", + "version": "1.116.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "my-weekly-todo-list", - "version": "1.116.2", + "version": "1.116.3", "license": "MIT", "dependencies": { "@auth/prisma-adapter": "^2.11.1", diff --git a/package.json b/package.json index 30e68b9..581c9f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.116.2", + "version": "1.116.3", "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": { diff --git a/src/app/api/calendar/events/route.ts b/src/app/api/calendar/events/route.ts index b3b8271..5601822 100644 --- a/src/app/api/calendar/events/route.ts +++ b/src/app/api/calendar/events/route.ts @@ -109,7 +109,7 @@ export async function PATCH(request: NextRequest) { if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const body = await request.json(); - const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, url, + const { calendarId, originalCalendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, timezone, url, reminders, busyStatus, visibility, attendees, attachments, editMode, recurringEventId } = body; console.log('[API] Updating event:', { calendarId, eventId, title, editMode }); @@ -128,6 +128,14 @@ export async function PATCH(request: NextRequest) { const userId = dbUser2?.id; if (!userId) return NextResponse.json({ error: 'User not found' }, { status: 404 }); + const sourceCalendarId = originalCalendarId || calendarId; + const sourceConnection = sourceCalendarId === calendarId + ? connection + : await findConnectionForCalendar(session.user.email, sourceCalendarId); + if (!sourceConnection) { + return NextResponse.json({ error: 'Original calendar connection not found' }, { status: 404 }); + } + // For "all events" edit mode on recurring events, use the series master ID let targetEventId = eventId; if (editMode === 'all' && recurringEventId) { @@ -139,25 +147,35 @@ export async function PATCH(request: NextRequest) { } } - const event = await updateCalendarEvent(connection, calendarId, targetEventId, { - title, - description, - start, - end, - location, + const eventData = { + title, description, start, end, location, allDay: allDay !== undefined ? !!allDay : undefined, - recurrence, - recurrenceEndDate, - recurrenceCount, - recurrenceInterval, - recurrenceDays, - url, - reminders, - busyStatus, - visibility, - attendees, - attachments, - } as any); + recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, timezone, + url, reminders, busyStatus, visibility, attendees, attachments, + } as any; + + // Providers do not share a portable move operation: create in the target, + // then remove the original event from its source calendar. + if (sourceCalendarId !== calendarId) { + if (editMode && editMode !== 'all') { + return NextResponse.json({ error: 'Recurring event instances must be moved as a whole series' }, { status: 400 }); + } + const movedEvent = await createCalendarEvent(connection, calendarId, eventData); + if (!movedEvent.backgroundColor && connection.calendars) { + const cal = (connection.calendars as any[]).find((c: any) => c.id === calendarId); + if (cal?.backgroundColor || cal?.color) movedEvent.backgroundColor = cal.backgroundColor || cal.color; + } + await deleteCalendarEvent(sourceConnection, sourceCalendarId, targetEventId, 'all'); + try { + await deleteCachedEvent(userId, targetEventId, sourceConnection.provider); + await upsertCachedEvent(userId, connection.id, connection.provider, movedEvent); + } catch (e) { + console.error('[CACHE] Failed to update cache after moving event:', e); + } + return NextResponse.json({ event: movedEvent }); + } + + const event = await updateCalendarEvent(connection, calendarId, targetEventId, eventData); // Attach calendar color from connection metadata for cache + frontend if (!event.backgroundColor && connection.calendars) { diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index 30ee5d8..6127bfc 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -364,6 +364,7 @@ export default function CalendarEventModal({ recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined, recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined, calendarId, + ...(event?.calendarId ? { originalCalendarId: event.calendarId } : {}), allDay, start: { dateTime: startDate.toISOString() }, end: { dateTime: endDate.toISOString() }, @@ -601,22 +602,22 @@ export default function CalendarEventModal({
{isCalendarSelectorOpen && ( diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 2764410..ddd8ff4 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -1838,7 +1838,7 @@ export default function WeeklyView() { source: ev.source || calInfo?.provider || 'google', calendarId: ev.calendarId || eventData.calendarId, calendarTitle: ev.calendarTitle || calInfo?.summary || calInfo?.title || '', - calendarColor: ev.backgroundColor || ev.calendarColor || calInfo?.backgroundColor || calInfo?.color || '#3b82f6', + calendarColor: calInfo?.backgroundColor || calInfo?.color || ev.backgroundColor || ev.calendarColor || '#3b82f6', }; setRawCalendarEvents(prev => { if (eventData.id) { @@ -1860,13 +1860,17 @@ export default function WeeklyView() { source: ev.source || calInfo?.provider || 'google', calendarId: ev.calendarId || eventData.calendarId, calendarTitle: ev.calendarTitle || calInfo?.summary || calInfo?.title || '', - calendarColor: ev.backgroundColor || ev.calendarColor || calInfo?.backgroundColor || calInfo?.color || '#3b82f6', + calendarColor: calInfo?.backgroundColor || calInfo?.color || ev.backgroundColor || ev.calendarColor || '#3b82f6', }; setRawCalendarEvents(prev => prev.map(e => e.id === eventData.id ? frontendEvent : e)); } // Force refresh only the affected provider - const connId = getConnectionIdForCalendar(eventData.calendarId); - await fetchCalendarEvents(true, connId); + const targetConnId = getConnectionIdForCalendar(eventData.calendarId); + const sourceConnId = getConnectionIdForCalendar(eventData.originalCalendarId); + await fetchCalendarEvents(true, targetConnId); + if (sourceConnId && sourceConnId !== targetConnId) { + await fetchCalendarEvents(true, sourceConnId); + } } catch (error: any) { console.error("Error saving event:", error); if (error.name === "AbortError") { @@ -7803,11 +7807,14 @@ export default function WeeklyView() { // Convert hex to rgba for background, or use default const eventColor = event.calendarColor || "#009a9a"; const bgColor = eventColor.startsWith("#") - ? `${eventColor}20` // Add alpha for transparency + ? `color-mix(in srgb, ${eventColor} 14%, white)` : eventColor; const borderColor = eventColor.startsWith("#") ? eventColor : "var(--weekly-teal)"; + const textColor = eventColor.startsWith("#") + ? `color-mix(in srgb, ${eventColor} 78%, black)` + : borderColor; const overlap = eventOverlapLayout[event.id] || { column: 0, totalColumns: 1 }; const colWidth = 100 / overlap.totalColumns; @@ -7831,7 +7838,7 @@ export default function WeeklyView() { justifyContent: "flex-start", backgroundColor: bgColor, borderLeftColor: borderColor, - color: borderColor, + color: textColor, cursor: event.editable ? (isDragging ? "grabbing" : "grab") : "default", @@ -7965,11 +7972,14 @@ export default function WeeklyView() { {getEventsForDate(date).map((event) => { const eventColor = event.calendarColor || "#009a9a"; const bgColor = eventColor.startsWith("#") - ? `${eventColor}20` + ? `color-mix(in srgb, ${eventColor} 14%, white)` : eventColor; const borderColor = eventColor.startsWith("#") ? eventColor : "var(--weekly-teal)"; + const textColor = eventColor.startsWith("#") + ? `color-mix(in srgb, ${eventColor} 78%, black)` + : borderColor; return (
diff --git a/src/lib/calendar-events.ts b/src/lib/calendar-events.ts index 6ec822f..36fa4a0 100644 --- a/src/lib/calendar-events.ts +++ b/src/lib/calendar-events.ts @@ -351,7 +351,10 @@ export const getCalendarEvents = async ( const calendarData = calendars.find(c => c.id === calendarId); events = events.concat(calendarEvents.map((event: any) => { - const eventColor = event.colorId ? getGoogleEventColor(event.colorId) : calendarData?.backgroundColor; + // Keep the event tone consistent with the calendar selected by the user. + // Google can provide an event-specific color which otherwise makes one + // item look unrelated to the rest of its calendar. + const eventColor = calendarData?.backgroundColor || (event.colorId ? getGoogleEventColor(event.colorId) : undefined); // Map Google reminders to our format const reminders: EventReminder[] | undefined = event.reminders?.overrides?.map((r: any) => ({ method: r.method === 'email' ? 'email' : 'popup', @@ -1406,4 +1409,4 @@ export const deleteCalendarEvent = async ( } throw new Error(`Provider ${connection.provider} does not support deleting events yet.`); -}; \ No newline at end of file +};