feat(05-04): calendar API client, calendar widget, registry wiring
- calendar-api.ts: fetchSources/addSource/updateSource/deleteSource/testSource/fetchEvents with credentials:'include' - calendar-widget.tsx: upcoming-events list with source color dots, three empty states (no sources/no events/loading) - widget-registry.tsx: wireCalendarWidget() replaces placeholder with real CalendarWidget - page.tsx: wires CalendarWidget into registry on mount - i18n: added calendar.loading key to de.json and en.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Calendar API client functions.
|
||||
* Consumes the calendar backend built in 05-03.
|
||||
* All calls use credentials: 'include' for cookie-based auth.
|
||||
*
|
||||
* Anti-pattern (RESEARCH): The browser NEVER fetches external calendars directly.
|
||||
* All event data flows through /api/calendar/events which proxies via the backend.
|
||||
*/
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
// --- Types mirroring the 05-03 backend contract ---
|
||||
|
||||
/**
|
||||
* Calendar source as returned by GET /calendar/sources.
|
||||
* Note: encryptedPassword is NEVER returned (T-05-09).
|
||||
*/
|
||||
export interface CalendarSource {
|
||||
id: string;
|
||||
userId: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
type: 'caldav' | 'ics' | 'exchange';
|
||||
exchangeMode?: string | null;
|
||||
url: string;
|
||||
username?: string | null;
|
||||
color?: string | null;
|
||||
isVisible: boolean;
|
||||
syncIntervalMin: number;
|
||||
lastSyncAt?: string | null;
|
||||
lastSyncError?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized calendar event from GET /calendar/events.
|
||||
* Mirrors CalendarEvent from calendar.service.ts.
|
||||
*/
|
||||
export interface CalendarEvent {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
title: string;
|
||||
start: string;
|
||||
end: string;
|
||||
allDay: boolean;
|
||||
location?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for creating a new calendar source.
|
||||
*/
|
||||
export interface CreateSourcePayload {
|
||||
name: string;
|
||||
type: 'caldav' | 'ics' | 'exchange';
|
||||
url: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
exchangeMode?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for updating an existing calendar source.
|
||||
* All fields are optional.
|
||||
*/
|
||||
export interface UpdateSourcePayload {
|
||||
name?: string;
|
||||
type?: 'caldav' | 'ics' | 'exchange';
|
||||
url?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
exchangeMode?: string;
|
||||
color?: string;
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
// --- API functions ---
|
||||
|
||||
/**
|
||||
* Fetch all calendar sources for the current user.
|
||||
* GET /calendar/sources
|
||||
*/
|
||||
export async function fetchSources(): Promise<CalendarSource[]> {
|
||||
const res = await fetch(`${API_URL}/calendar/sources`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch calendar sources');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new calendar source.
|
||||
* POST /calendar/sources
|
||||
*/
|
||||
export async function addSource(
|
||||
payload: CreateSourcePayload,
|
||||
): Promise<CalendarSource> {
|
||||
const res = await fetch(`${API_URL}/calendar/sources`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to add calendar source');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing calendar source (including isVisible toggle for CAL-02).
|
||||
* PATCH /calendar/sources/:id
|
||||
*/
|
||||
export async function updateSource(
|
||||
id: string,
|
||||
payload: UpdateSourcePayload,
|
||||
): Promise<CalendarSource> {
|
||||
const res = await fetch(`${API_URL}/calendar/sources/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update calendar source');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calendar source.
|
||||
* DELETE /calendar/sources/:id
|
||||
*/
|
||||
export async function deleteSource(id: string): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/calendar/sources/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to delete calendar source');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to a calendar source.
|
||||
* POST /calendar/sources/:id/test
|
||||
* Returns { success: boolean } from the backend.
|
||||
*/
|
||||
export async function testSource(
|
||||
id: string,
|
||||
): Promise<{ success: boolean }> {
|
||||
const res = await fetch(`${API_URL}/calendar/sources/${id}/test`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to test calendar source');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch aggregated events from all visible sources.
|
||||
* GET /calendar/events?from=...&to=...
|
||||
* Default window: now to now + 30 days (backend default).
|
||||
*/
|
||||
export async function fetchEvents(
|
||||
from?: string,
|
||||
to?: string,
|
||||
): Promise<CalendarEvent[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.set('from', from);
|
||||
if (to) params.set('to', to);
|
||||
|
||||
const queryStr = params.toString();
|
||||
const url = `${API_URL}/calendar/events${queryStr ? `?${queryStr}` : ''}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch calendar events');
|
||||
return res.json();
|
||||
}
|
||||
Reference in New Issue
Block a user