My-Weekly-ToDo-List/src/lib/google-calendar.ts
mARTin 1e789d93ae feat: cross-day drag, note icon, Google recurring fix, no popup on drag
- Fix Google recurring events missing isRecurring flag (recurringEventId
  was not mapped from Google API response)
- Add note icon (FileText) on events with descriptions, tooltip on hover
- Cross-day drag-and-drop: detect day column under cursor and shift date
- Fix event detail popup opening after drag/resize by setting ref before
  async operations
- Add data-date attribute to day columns for drag target detection
- Use CSS class for icon row layout to prevent overlap

v1.67.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 12:03:33 +01:00

247 lines
6.8 KiB
TypeScript

// Google Calendar OAuth wrapper
import { google } from 'googleapis';
export interface GoogleCalendarEvent {
id: string;
summary: string;
description?: string;
start: {
dateTime?: string;
date?: string;
};
end: {
dateTime?: string;
date?: string;
};
attendees?: Array<{
email: string;
displayName?: string;
responseStatus?: string;
}>;
location?: string;
colorId?: string;
reminders?: {
useDefault: boolean;
overrides?: Array<{ method: string; minutes: number }>;
};
transparency?: string; // 'opaque' | 'transparent'
visibility?: string; // 'default' | 'public' | 'private' | 'confidential'
recurrence?: string[];
}
export interface GoogleCalendar {
id: string;
summary: string;
primary?: boolean;
backgroundColor?: string;
}
/**
* Initialize Google Calendar OAuth client
*/
export const initializeOAuth = (clientId: string, clientSecret: string, redirectUri: string) => {
const oauth2Client = new google.auth.OAuth2(
clientId,
clientSecret,
redirectUri
);
return oauth2Client;
};
/**
* Get user calendar list using OAuth token
*/
export const getUserCalendars = async (
oauth2Client: any,
accessToken: string
): Promise<GoogleCalendar[]> => {
// Set access token
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const response = await calendar.calendarList.list();
return response.data.items?.map((item: any) => ({
id: item.id,
summary: item.summary,
primary: item.primary,
backgroundColor: item.backgroundColor,
})) || [];
} catch (error) {
console.error('Error fetching user calendars:', error);
throw new Error('Failed to fetch user calendars');
}
};
/**
* Get upcoming events for a specified time period
*/
export const getUpcomingEvents = async (
oauth2Client: any,
accessToken: string,
calendarId: string,
timeMin: string,
timeMax: string
): Promise<GoogleCalendarEvent[]> => {
// Set access token
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const response = await calendar.events.list({
calendarId,
timeMin,
timeMax,
singleEvents: true,
orderBy: 'startTime',
});
return response.data.items?.map((item: any) => ({
id: item.id,
summary: item.summary,
description: item.description,
start: item.start,
end: item.end,
attendees: item.attendees?.map((a: any) => ({
email: a.email,
displayName: a.displayName,
responseStatus: a.responseStatus,
})),
location: item.location,
colorId: item.colorId,
reminders: item.reminders,
transparency: item.transparency,
visibility: item.visibility,
recurringEventId: item.recurringEventId,
})) || [];
} catch (error) {
console.error('Error fetching upcoming events:', error);
throw new Error('Failed to fetch upcoming events');
}
};
/**
* Create a new event
*/
export const createEvent = async (
oauth2Client: any,
accessToken: string,
calendarId: string,
event: Partial<GoogleCalendarEvent>
): Promise<GoogleCalendarEvent> => {
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const requestBody: any = {
summary: event.summary,
description: event.description,
start: event.start,
end: event.end,
location: event.location,
};
if ((event as any).recurrence) requestBody.recurrence = (event as any).recurrence;
if ((event as any).source) requestBody.source = (event as any).source;
if (event.reminders) requestBody.reminders = event.reminders;
if (event.transparency) requestBody.transparency = event.transparency;
if (event.visibility) requestBody.visibility = event.visibility;
if (event.attendees) requestBody.attendees = event.attendees;
const response = await calendar.events.insert({
calendarId,
requestBody,
});
return response.data as any;
} catch (error) {
console.error('Error creating event:', error);
throw new Error('Failed to create event');
}
};
/**
* Update an existing event
*/
export const updateEvent = async (
oauth2Client: any,
accessToken: string,
calendarId: string,
eventId: string,
event: Partial<GoogleCalendarEvent>
): Promise<GoogleCalendarEvent> => {
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const requestBody: any = {
summary: event.summary,
description: event.description,
start: event.start,
end: event.end,
location: event.location,
};
if ((event as any).recurrence) requestBody.recurrence = (event as any).recurrence;
if ((event as any).source) requestBody.source = (event as any).source;
if (event.reminders) requestBody.reminders = event.reminders;
if (event.transparency) requestBody.transparency = event.transparency;
if (event.visibility) requestBody.visibility = event.visibility;
if (event.attendees) requestBody.attendees = event.attendees;
const response = await calendar.events.patch({
calendarId,
eventId,
requestBody,
});
return response.data as any;
} catch (error) {
console.error('Error updating event:', error);
throw new Error('Failed to update event');
}
};
/**
* Get a single event by ID
*/
export const getEvent = async (
oauth2Client: any,
accessToken: string,
calendarId: string,
eventId: string
): Promise<GoogleCalendarEvent | null> => {
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const response = await calendar.events.get({
calendarId,
eventId,
});
return response.data as any;
} catch (error) {
console.error('Error getting event:', error);
return null;
}
};
/**
* Delete an event
*/
export const deleteEvent = async (
oauth2Client: any,
accessToken: string,
calendarId: string,
eventId: string
): Promise<void> => {
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
await calendar.events.delete({
calendarId,
eventId,
});
} catch (error: any) {
// 410 Gone means already deleted — treat as success
if (error?.code === 410 || error?.status === 410) {
console.log('[GOOGLE] Event already deleted (410 Gone), treating as success');
return;
}
console.error('Error deleting event:', error);
throw new Error('Failed to delete event');
}
};