♻️(front) update calendar API and types

Update API functions and types for subscription management
and iCal export. Add fetchApi improvements for error handling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Panchout
2026-01-25 20:35:06 +01:00
parent a5c10b2ca9
commit 51033d7457
4 changed files with 171 additions and 37 deletions

View File

@@ -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<string, never>;
export const fetchAPI = async (
input: string,
init?: RequestInit & { params?: Record<string, string | number> },
options?: fetchAPIOptions
) => {
const apiUrl = new URL(`${baseApiUrl("1.0")}${input}`);
if (init?.params) {

View File

@@ -5,14 +5,24 @@ export interface ApiConfig {
FRONTEND_FEEDBACK_BUTTON_IDLE?: boolean;
FRONTEND_FEEDBACK_ITEMS?: Record<string, { url: string }>;
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<string, unknown>;
export interface UserFilters {
[key: string]: unknown;
}
export interface ThemeCustomization {
footer?: LocalizedRecord;
[key: string]: LocalizedRecord | unknown;
}
export interface LocalizedRecord {
default?: Record<string, unknown>;
[languageCode: string]: Record<string, unknown> | undefined;
}

View File

@@ -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 ||

View File

@@ -15,20 +15,33 @@ export interface Calendar {
}
/**
* Paginated API response.
*/
interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
/**
* Fetch all calendars accessible by the current user.
*/
export const getCalendars = async (): Promise<Calendar[]> => {
const response = await fetchAPI("calendars/");
return response.json();
const data: PaginatedResponse<Calendar> = 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<Calendar> => {
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<Calendar> => {
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<void> => {
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<GetSubscriptionTokenResult> => {
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<SubscriptionToken> => {
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<void> => {
await fetchAPI(
`subscription-tokens/by-path/?caldav_path=${encodeURIComponent(caldavPath)}`,
{
method: "DELETE",
}
);
};