New features user management Google Calendar integration

This commit is contained in:
AlacrisDevs
2026-02-04 23:53:34 +02:00
parent cfec43f7ef
commit 6ec6b0753f
14 changed files with 1847 additions and 328 deletions

View File

@@ -1,264 +1,100 @@
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '$lib/supabase/types';
// Google Calendar functions for PUBLIC calendars (no OAuth needed)
// Just needs a Google API key and a public calendar ID
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = import.meta.env.VITE_GOOGLE_CLIENT_SECRET;
interface GoogleTokens {
access_token: string;
refresh_token: string;
expires_in: number;
}
interface GoogleCalendarEvent {
export interface GoogleCalendarEvent {
id: string;
summary: string;
description?: string;
start: { dateTime?: string; date?: string };
end: { dateTime?: string; date?: string };
colorId?: string;
htmlLink?: string;
}
export function getGoogleAuthUrl(redirectUri: string, state: string): string {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events',
access_type: 'offline',
prompt: 'consent',
state
});
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
}
/**
* Extract calendar ID from various Google Calendar URL formats
* Supports:
* - Direct calendar ID (email format)
* - Shareable URL with cid parameter (base64 encoded)
* - Public URL with calendar ID in path
*/
export function extractCalendarId(input: string): string | null {
if (!input) return null;
export async function exchangeCodeForTokens(code: string, redirectUri: string): Promise<GoogleTokens> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: redirectUri,
grant_type: 'authorization_code'
})
});
if (!response.ok) {
throw new Error('Failed to exchange code for tokens');
// If it looks like an email/calendar ID already, return it
if (input.includes('@') && !input.includes('http')) {
return input.trim();
}
return response.json();
}
try {
const url = new URL(input);
export async function refreshAccessToken(refreshToken: string): Promise<GoogleTokens> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
refresh_token: refreshToken,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
grant_type: 'refresh_token'
})
});
// Check for cid parameter (base64 encoded calendar ID)
const cid = url.searchParams.get('cid');
if (cid) {
// Decode base64 to get calendar ID
try {
return atob(cid);
} catch {
return cid; // Maybe it's not encoded
}
}
if (!response.ok) {
throw new Error('Failed to refresh access token');
}
// Check for src parameter
const src = url.searchParams.get('src');
if (src) return src;
return response.json();
}
// Check path for calendar ID in ical URL format
const icalMatch = input.match(/calendar\/ical\/([^/]+)/);
if (icalMatch) return decodeURIComponent(icalMatch[1]);
export async function getValidAccessToken(
supabase: SupabaseClient<Database>,
userId: string
): Promise<string | null> {
const { data: connection } = await supabase
.from('google_calendar_connections')
.select('*')
.eq('user_id', userId)
.single();
if (!connection) return null;
const expiresAt = new Date(connection.token_expires_at);
const now = new Date();
// Refresh if expires within 5 minutes
if (expiresAt.getTime() - now.getTime() < 5 * 60 * 1000) {
try {
const tokens = await refreshAccessToken(connection.refresh_token);
const newExpiresAt = new Date(Date.now() + tokens.expires_in * 1000);
await supabase
.from('google_calendar_connections')
.update({
access_token: tokens.access_token,
token_expires_at: newExpiresAt.toISOString(),
updated_at: new Date().toISOString()
})
.eq('user_id', userId);
return tokens.access_token;
} catch {
return null;
} catch {
// Not a valid URL, maybe it's just a calendar ID
if (input.includes('@')) {
return input.trim();
}
}
return connection.access_token;
return null;
}
export async function fetchGoogleCalendarEvents(
accessToken: string,
calendarId: string = 'primary',
/**
* Generate a subscribe URL for a public Google Calendar
*/
export function getCalendarSubscribeUrl(calendarId: string): string {
const encoded = btoa(calendarId);
return `https://calendar.google.com/calendar/u/0?cid=${encoded}`;
}
/**
* Fetch events from a PUBLIC Google Calendar
* Requires: Calendar must be set to "Make available to public" in Google Calendar settings
*/
export async function fetchPublicCalendarEvents(
calendarId: string,
apiKey: string,
timeMin: Date,
timeMax: Date
): Promise<GoogleCalendarEvent[]> {
const params = new URLSearchParams({
key: apiKey,
timeMin: timeMin.toISOString(),
timeMax: timeMax.toISOString(),
singleEvents: 'true',
orderBy: 'startTime'
orderBy: 'startTime',
maxResults: '250'
});
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?${params}`,
{
headers: { Authorization: `Bearer ${accessToken}` }
}
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?${params}`
);
if (!response.ok) {
throw new Error('Failed to fetch Google Calendar events');
const error = await response.text();
console.error('Google Calendar API error:', error);
throw new Error('Failed to fetch calendar events. Make sure the calendar is set to public.');
}
const data = await response.json();
return data.items ?? [];
}
export async function createGoogleCalendarEvent(
accessToken: string,
calendarId: string = 'primary',
event: {
title: string;
description?: string;
startTime: string;
endTime: string;
allDay?: boolean;
}
): Promise<GoogleCalendarEvent> {
const body: Record<string, unknown> = {
summary: event.title,
description: event.description
};
if (event.allDay) {
const startDate = event.startTime.split('T')[0];
const endDate = event.endTime.split('T')[0];
body.start = { date: startDate };
body.end = { date: endDate };
} else {
body.start = { dateTime: event.startTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
body.end = { dateTime: event.endTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
}
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}
);
if (!response.ok) {
throw new Error('Failed to create Google Calendar event');
}
return response.json();
}
export async function updateGoogleCalendarEvent(
accessToken: string,
calendarId: string = 'primary',
eventId: string,
event: {
title: string;
description?: string;
startTime: string;
endTime: string;
allDay?: boolean;
}
): Promise<GoogleCalendarEvent> {
const body: Record<string, unknown> = {
summary: event.title,
description: event.description
};
if (event.allDay) {
const startDate = event.startTime.split('T')[0];
const endDate = event.endTime.split('T')[0];
body.start = { date: startDate };
body.end = { date: endDate };
} else {
body.start = { dateTime: event.startTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
body.end = { dateTime: event.endTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
}
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}
);
if (!response.ok) {
throw new Error('Failed to update Google Calendar event');
}
return response.json();
}
export async function deleteGoogleCalendarEvent(
accessToken: string,
calendarId: string = 'primary',
eventId: string
): Promise<void> {
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`,
{
method: 'DELETE',
headers: { Authorization: `Bearer ${accessToken}` }
}
);
if (!response.ok && response.status !== 404) {
throw new Error('Failed to delete Google Calendar event');
}
}
export async function listGoogleCalendars(accessToken: string): Promise<{ id: string; summary: string }[]> {
const response = await fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList', {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!response.ok) {
throw new Error('Failed to list calendars');
}
const data = await response.json();
return (data.items ?? []).map((cal: { id: string; summary: string }) => ({
id: cal.id,
summary: cal.summary
}));
}

View File

@@ -31,6 +31,7 @@ export const load: LayoutServerLoad = async ({ params, locals }) => {
return {
org,
role: membership.role
role: membership.role,
userRole: membership.role
};
};

View File

@@ -6,13 +6,18 @@
data: {
org: { id: string; name: string; slug: string };
role: string;
userRole: string;
};
children: Snippet;
}
let { data, children }: Props = $props();
const navItems = [
const isAdmin = $derived(
data.userRole === "owner" || data.userRole === "admin",
);
const navItems = $derived([
{ href: `/${data.org.slug}`, label: "Overview", icon: "home" },
{
href: `/${data.org.slug}/documents`,
@@ -25,12 +30,17 @@
label: "Calendar",
icon: "calendar",
},
{
href: `/${data.org.slug}/settings`,
label: "Settings",
icon: "settings",
},
];
// Only show settings for admins
...(isAdmin
? [
{
href: `/${data.org.slug}/settings`,
label: "Settings",
icon: "settings",
},
]
: []),
]);
function isActive(href: string): boolean {
return $page.url.pathname === href;

View File

@@ -1,7 +1,7 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent, locals }) => {
const { org } = await parent();
const { org, userRole } = await parent();
const { supabase } = locals;
// Fetch events for current month ± 1 month
@@ -18,6 +18,7 @@ export const load: PageServerLoad = async ({ parent, locals }) => {
.order('start_time');
return {
events: events ?? []
events: events ?? [],
userRole
};
};

View File

@@ -1,8 +1,12 @@
<script lang="ts">
import { getContext } from "svelte";
import { getContext, onMount } from "svelte";
import { Button, Modal, Input, Textarea } from "$lib/components/ui";
import { Calendar } from "$lib/components/calendar";
import { createEvent } from "$lib/api/calendar";
import {
getCalendarSubscribeUrl,
type GoogleCalendarEvent,
} from "$lib/api/google-calendar";
import type { CalendarEvent } from "$lib/supabase/types";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
@@ -12,6 +16,7 @@
org: { id: string; name: string; slug: string };
events: CalendarEvent[];
user: { id: string } | null;
userRole?: string;
};
}
@@ -20,6 +25,17 @@
const supabase = getContext<SupabaseClient<Database>>("supabase");
let events = $state(data.events);
let googleEvents = $state<CalendarEvent[]>([]);
let isOrgCalendarConnected = $state(false);
let isLoadingGoogle = $state(false);
let orgCalendarId = $state<string | null>(null);
let orgCalendarName = $state<string | null>(null);
const isAdmin = $derived(
data.userRole === "owner" || data.userRole === "admin",
);
const allEvents = $derived([...events, ...googleEvents]);
let showCreateModal = $state(false);
let showEventModal = $state(false);
let selectedEvent = $state<CalendarEvent | null>(null);
@@ -103,11 +119,100 @@
const end = new Date(event.end_time);
return `${start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} - ${end.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
}
onMount(async () => {
await loadGoogleCalendarEvents();
});
async function loadGoogleCalendarEvents() {
isLoadingGoogle = true;
try {
const res = await fetch(
`/api/google-calendar/events?org_id=${data.org.id}`,
);
const result = await res.json();
// Check if calendar is connected (even if no events)
if (res.ok && result.calendar_id) {
isOrgCalendarConnected = true;
orgCalendarId = result.calendar_id;
orgCalendarName = result.calendar_name;
if (result.events && result.events.length > 0) {
googleEvents = result.events.map(
(ge: GoogleCalendarEvent) => ({
id: `google-${ge.id}`,
org_id: data.org.id,
title: ge.summary || "(No title)",
description: ge.description ?? null,
start_time:
ge.start.dateTime ||
`${ge.start.date}T00:00:00`,
end_time:
ge.end.dateTime || `${ge.end.date}T23:59:59`,
all_day: !ge.start.dateTime,
color: "#4285f4",
recurrence: null,
created_by: data.user?.id ?? "",
created_at: new Date().toISOString(),
}),
) as CalendarEvent[];
}
} else if (result.error) {
console.error("Calendar API error:", result.error);
}
} catch (e) {
console.error("Failed to load Google events:", e);
}
isLoadingGoogle = false;
}
function subscribeToCalendar() {
if (!orgCalendarId) return;
const url = getCalendarSubscribeUrl(orgCalendarId);
window.open(url, "_blank");
}
</script>
<div class="p-6 h-full overflow-auto">
<header class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-light">Calendar</h1>
<div class="flex items-center gap-4">
<h1 class="text-2xl font-bold text-light">Calendar</h1>
{#if isOrgCalendarConnected}
<div class="flex items-center gap-2">
<span
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-500/10 text-blue-400 rounded-lg"
>
<svg class="w-4 h-4" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
</svg>
{orgCalendarName ?? "Google Calendar"}
{#if isLoadingGoogle}
<span class="animate-spin"></span>
{/if}
</span>
<button
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-green-500/10 text-green-400 rounded-lg hover:bg-green-500/20 transition-colors"
onclick={subscribeToCalendar}
title="Add to your Google Calendar"
>
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M12 5v14M5 12h14" />
</svg>
Subscribe
</button>
</div>
{/if}
</div>
<Button onclick={() => (showCreateModal = true)}>
<svg
class="w-4 h-4 mr-2"
@@ -124,7 +229,7 @@
</header>
<Calendar
{events}
events={allEvents}
onDateClick={handleDateClick}
onEventClick={handleEventClick}
/>
@@ -208,32 +313,130 @@
title={selectedEvent?.title ?? "Event"}
>
{#if selectedEvent}
<div class="space-y-3">
<div class="flex items-center gap-2">
<div class="space-y-4">
<!-- Date and Time -->
<div class="flex items-start gap-3">
<div
class="w-3 h-3 rounded-full"
style="background-color: {selectedEvent.color ?? '#6366f1'}"
></div>
<span class="text-light/70"
>{formatEventTime(selectedEvent)}</span
class="w-8 h-8 rounded-lg bg-light/10 flex items-center justify-center flex-shrink-0"
>
<svg
class="w-4 h-4 text-light/70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="4"
width="18"
height="18"
rx="2"
ry="2"
/>
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
</div>
<div>
<p class="text-light font-medium">
{new Date(selectedEvent.start_time).toLocaleDateString(
undefined,
{
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
},
)}
</p>
<p class="text-light/60 text-sm">
{formatEventTime(selectedEvent)}
</p>
</div>
</div>
<!-- Description -->
{#if selectedEvent.description}
<p class="text-light/80">{selectedEvent.description}</p>
<div class="flex items-start gap-3">
<div
class="w-8 h-8 rounded-lg bg-light/10 flex items-center justify-center flex-shrink-0"
>
<svg
class="w-4 h-4 text-light/70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="17" y1="10" x2="3" y2="10" />
<line x1="21" y1="6" x2="3" y2="6" />
<line x1="21" y1="14" x2="3" y2="14" />
<line x1="17" y1="18" x2="3" y2="18" />
</svg>
</div>
<p class="text-light/80 text-sm whitespace-pre-wrap">
{selectedEvent.description}
</p>
</div>
{/if}
<p class="text-xs text-light/40">
{new Date(selectedEvent.start_time).toLocaleDateString(
undefined,
{
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
},
)}
</p>
<!-- Color indicator -->
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-lg bg-light/10 flex items-center justify-center flex-shrink-0"
>
<div
class="w-4 h-4 rounded-full"
style="background-color: {selectedEvent.color ??
'#6366f1'}"
></div>
</div>
<span class="text-light/60 text-sm">
{selectedEvent.id.startsWith("google-")
? "Google Calendar Event"
: "Local Event"}
</span>
</div>
<!-- Google Calendar link -->
{#if selectedEvent.id.startsWith("google-") && orgCalendarId}
<div class="pt-3 border-t border-light/10">
<a
href="https://calendar.google.com/calendar/u/0/r/eventedit/{selectedEvent.id.replace(
'google-',
'',
)}?cid={encodeURIComponent(orgCalendarId)}"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-4 py-2 text-sm bg-blue-500/20 text-blue-400 rounded-lg hover:bg-blue-500/30 transition-colors"
>
<svg class="w-4 h-4" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Open in Google Calendar
</a>
<p class="text-xs text-light/40 mt-2">
Edit this event directly in Google Calendar
</p>
</div>
{/if}
</div>
{/if}
</Modal>

View File

@@ -0,0 +1,61 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent, locals }) => {
const { org, userRole } = await parent();
// Only admins and owners can access settings
if (userRole !== 'owner' && userRole !== 'admin') {
redirect(303, `/${(org as any).slug}`);
}
const orgId = (org as any).id;
// Get org members with profiles
const { data: members } = await locals.supabase
.from('org_members')
.select(`
id,
user_id,
role,
role_id,
created_at,
profiles:user_id (
id,
email,
full_name,
avatar_url
)
`)
.eq('org_id', orgId);
// Get org roles
const { data: roles } = await locals.supabase
.from('org_roles')
.select('*')
.eq('org_id', orgId)
.order('position');
// Get pending invites
const { data: invites } = await locals.supabase
.from('org_invites')
.select('*')
.eq('org_id', orgId)
.is('accepted_at', null)
.gt('expires_at', new Date().toISOString());
// Get org Google Calendar connection
const { data: orgCalendar } = await locals.supabase
.from('org_google_calendars')
.select('*')
.eq('org_id', orgId)
.single();
return {
members: members ?? [],
roles: roles ?? [],
invites: invites ?? [],
orgCalendar,
userRole
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { exchangeCodeForTokens } from '$lib/api/google-calendar';
export const GET: RequestHandler = async ({ url, locals }) => {
const code = url.searchParams.get('code');
const stateParam = url.searchParams.get('state');
const error = url.searchParams.get('error');
if (error || !code || !stateParam) {
redirect(303, '/?error=google_auth_failed');
}
let state: { orgSlug: string; userId: string };
try {
state = JSON.parse(decodeURIComponent(stateParam));
} catch {
redirect(303, '/?error=invalid_state');
}
const redirectUri = `${url.origin}/api/google-calendar/callback`;
try {
const tokens = await exchangeCodeForTokens(code, redirectUri);
const expiresAt = new Date(Date.now() + tokens.expires_in * 1000);
// Store tokens in database
await locals.supabase
.from('google_calendar_connections')
.upsert({
user_id: state.userId,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
token_expires_at: expiresAt.toISOString(),
updated_at: new Date().toISOString()
}, { onConflict: 'user_id' });
redirect(303, `/${state.orgSlug}/calendar?connected=true`);
} catch (err) {
console.error('Google Calendar OAuth error:', err);
redirect(303, `/${state.orgSlug}/calendar?error=token_exchange_failed`);
}
};

View File

@@ -1,22 +0,0 @@
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getGoogleAuthUrl } from '$lib/api/google-calendar';
export const GET: RequestHandler = async ({ url, locals }) => {
const { session } = await locals.safeGetSession();
if (!session) {
redirect(303, '/login');
}
const orgSlug = url.searchParams.get('org');
if (!orgSlug) {
redirect(303, '/');
}
const redirectUri = `${url.origin}/api/google-calendar/callback`;
const state = JSON.stringify({ orgSlug, userId: session.user.id });
const authUrl = getGoogleAuthUrl(redirectUri, encodeURIComponent(state));
redirect(303, authUrl);
};

View File

@@ -0,0 +1,60 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { GOOGLE_API_KEY } from '$env/static/private';
import { fetchPublicCalendarEvents } from '$lib/api/google-calendar';
// Fetch events from a public Google Calendar
export const GET: RequestHandler = async ({ url, locals }) => {
const orgId = url.searchParams.get('org_id');
if (!orgId) {
return json({ error: 'org_id required' }, { status: 400 });
}
if (!GOOGLE_API_KEY) {
return json({ error: 'Google API key not configured' }, { status: 500 });
}
try {
// Get the org's calendar ID from database
const { data: orgCal, error: dbError } = await locals.supabase
.from('org_google_calendars')
.select('calendar_id, calendar_name')
.eq('org_id', orgId)
.single();
if (dbError) {
console.error('DB error fetching calendar:', dbError);
return json({ error: 'No calendar connected', events: [] }, { status: 404 });
}
if (!orgCal) {
return json({ error: 'No calendar connected', events: [] }, { status: 404 });
}
console.log('Fetching events for calendar:', (orgCal as any).calendar_id);
// Fetch events for the next 3 months
const now = new Date();
const timeMin = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const timeMax = new Date(now.getFullYear(), now.getMonth() + 3, 0);
const events = await fetchPublicCalendarEvents(
(orgCal as any).calendar_id,
GOOGLE_API_KEY,
timeMin,
timeMax
);
console.log('Fetched', events.length, 'events');
return json({
events,
calendar_id: (orgCal as any).calendar_id,
calendar_name: (orgCal as any).calendar_name
});
} catch (err) {
console.error('Failed to fetch calendar events:', err);
return json({ error: 'Failed to fetch events. Make sure the calendar is public.', events: [] }, { status: 500 });
}
};