From 51033d74570f9b6f67e62ce1070cd509826d7021 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Sun, 25 Jan 2026 20:35:06 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(front)=20update=20calendar?= =?UTF-8?q?=20API=20and=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update API functions and types for subscription management and iCal export. Add fetchApi improvements for error handling. Co-Authored-By: Claude Opus 4.5 --- .../calendars/src/features/api/fetchApi.ts | 14 +- .../apps/calendars/src/features/api/types.ts | 14 +- .../apps/calendars/src/features/api/utils.ts | 19 --- .../calendars/src/features/calendar/api.ts | 161 +++++++++++++++++- 4 files changed, 171 insertions(+), 37 deletions(-) diff --git a/src/frontend/apps/calendars/src/features/api/fetchApi.ts b/src/frontend/apps/calendars/src/features/api/fetchApi.ts index d61c570..c718074 100644 --- a/src/frontend/apps/calendars/src/features/api/fetchApi.ts +++ b/src/frontend/apps/calendars/src/features/api/fetchApi.ts @@ -17,23 +17,11 @@ function getCSRFToken() { export const SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL = "redirect_after_login_url"; -const redirect = (url: string, saveRedirectAfterLoginUrl = true) => { - if (saveRedirectAfterLoginUrl) { - sessionStorage.setItem( - SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL, - window.location.href - ); - } - window.location.href = url; -}; - -export interface fetchAPIOptions { -} +export type fetchAPIOptions = Record; export const fetchAPI = async ( input: string, init?: RequestInit & { params?: Record }, - options?: fetchAPIOptions ) => { const apiUrl = new URL(`${baseApiUrl("1.0")}${input}`); if (init?.params) { diff --git a/src/frontend/apps/calendars/src/features/api/types.ts b/src/frontend/apps/calendars/src/features/api/types.ts index a181a2b..d906bc9 100644 --- a/src/frontend/apps/calendars/src/features/api/types.ts +++ b/src/frontend/apps/calendars/src/features/api/types.ts @@ -5,14 +5,24 @@ export interface ApiConfig { FRONTEND_FEEDBACK_BUTTON_IDLE?: boolean; FRONTEND_FEEDBACK_ITEMS?: Record; FRONTEND_MORE_LINK?: string; + FRONTEND_FEEDBACK_MESSAGES_WIDGET_ENABLED?: boolean; FRONTEND_FEEDBACK_MESSAGES_WIDGET_API_URL?: string; FRONTEND_FEEDBACK_MESSAGES_WIDGET_PATH?: string; FRONTEND_FEEDBACK_MESSAGES_WIDGET_CHANNEL?: string; theme_customization?: ThemeCustomization; } -export interface ThemeCustomization { - footer?: Record; +export interface UserFilters { [key: string]: unknown; } +export interface ThemeCustomization { + footer?: LocalizedRecord; + [key: string]: LocalizedRecord | unknown; +} + +export interface LocalizedRecord { + default?: Record; + [languageCode: string]: Record | undefined; +} + diff --git a/src/frontend/apps/calendars/src/features/api/utils.ts b/src/frontend/apps/calendars/src/features/api/utils.ts index 29838db..41b7a74 100644 --- a/src/frontend/apps/calendars/src/features/api/utils.ts +++ b/src/frontend/apps/calendars/src/features/api/utils.ts @@ -1,22 +1,3 @@ -export const errorCauses = async (response: Response, data?: unknown) => { - const errorsBody = (await response.json()) as Record< - string, - string | string[] - > | null; - - const causes = errorsBody - ? Object.entries(errorsBody) - .map(([, value]) => value) - .flat() - : undefined; - - return { - status: response.status, - cause: causes, - data, - }; -}; - export const getOrigin = () => { return ( process.env.NEXT_PUBLIC_API_ORIGIN || diff --git a/src/frontend/apps/calendars/src/features/calendar/api.ts b/src/frontend/apps/calendars/src/features/calendar/api.ts index 62c6799..959e271 100644 --- a/src/frontend/apps/calendars/src/features/calendar/api.ts +++ b/src/frontend/apps/calendars/src/features/calendar/api.ts @@ -15,20 +15,33 @@ export interface Calendar { } +/** + * Paginated API response. + */ +interface PaginatedResponse { + count: number; + next: string | null; + previous: string | null; + results: T[]; +} + /** * Fetch all calendars accessible by the current user. */ export const getCalendars = async (): Promise => { const response = await fetchAPI("calendars/"); - return response.json(); + const data: PaginatedResponse = await response.json(); + return data.results; }; /** - * Create a new calendar. + * Create a new calendar via Django API. + * This creates both the CalDAV calendar and the Django record. */ -export const createCalendar = async (data: { +export const createCalendarApi = async (data: { name: string; color?: string; + description?: string; }): Promise => { const response = await fetchAPI("calendars/", { method: "POST", @@ -37,6 +50,29 @@ export const createCalendar = async (data: { return response.json(); }; +/** + * Update an existing calendar via Django API. + */ +export const updateCalendarApi = async ( + calendarId: string, + data: { name?: string; color?: string; description?: string } +): Promise => { + const response = await fetchAPI(`calendars/${calendarId}/`, { + method: "PATCH", + body: JSON.stringify(data), + }); + return response.json(); +}; + +/** + * Delete a calendar via Django API. + */ +export const deleteCalendarApi = async (calendarId: string): Promise => { + await fetchAPI(`calendars/${calendarId}/`, { + method: "DELETE", + }); +}; + /** * Toggle calendar visibility. */ @@ -49,3 +85,122 @@ export const toggleCalendarVisibility = async ( return response.json(); }; +/** + * Subscription token for iCal export. + */ +export interface SubscriptionToken { + token: string; + url: string; + caldav_path: string; + calendar_name: string; + is_active: boolean; + last_accessed_at: string | null; + created_at: string; +} + +/** + * Parameters for subscription token operations. + */ +export interface SubscriptionTokenParams { + caldavPath: string; + calendarName?: string; +} + +/** + * Error types for subscription token operations. + */ +export type SubscriptionTokenError = + | { type: "not_found" } + | { type: "permission_denied"; message: string } + | { type: "network_error"; message: string } + | { type: "server_error"; message: string }; + +/** + * Result type for getSubscriptionToken - either a token, null (not found), or an error. + */ +export type GetSubscriptionTokenResult = + | { success: true; token: SubscriptionToken | null } + | { success: false; error: SubscriptionTokenError }; + +/** + * Get the subscription token for a calendar by CalDAV path. + * Returns a result object with either the token (or null if not found) or an error. + */ +export const getSubscriptionToken = async ( + caldavPath: string +): Promise => { + try { + const response = await fetchAPI( + `subscription-tokens/by-path/?caldav_path=${encodeURIComponent(caldavPath)}`, + { method: "GET" } + ); + return { success: true, token: await response.json() }; + } catch (error) { + if (error && typeof error === "object" && "status" in error) { + const status = error.status as number; + // 404 means no token exists yet - this is expected + if (status === 404) { + return { success: true, token: null }; + } + // Permission denied + if (status === 403) { + return { + success: false, + error: { + type: "permission_denied", + message: "You don't have access to this calendar", + }, + }; + } + // Server error + if (status >= 500) { + return { + success: false, + error: { + type: "server_error", + message: "Server error. Please try again later.", + }, + }; + } + } + // Network or unknown error + return { + success: false, + error: { + type: "network_error", + message: "Network error. Please check your connection.", + }, + }; + } +}; + +/** + * Create or get existing subscription token for a calendar. + */ +export const createSubscriptionToken = async ( + params: SubscriptionTokenParams +): Promise => { + const response = await fetchAPI("subscription-tokens/", { + method: "POST", + body: JSON.stringify({ + caldav_path: params.caldavPath, + calendar_name: params.calendarName || "", + }), + }); + return response.json(); +}; + +/** + * Delete (revoke) the subscription token for a calendar. + */ +export const deleteSubscriptionToken = async ( + caldavPath: string +): Promise => { + await fetchAPI( + `subscription-tokens/by-path/?caldav_path=${encodeURIComponent(caldavPath)}`, + { + method: "DELETE", + } + ); +}; +