5a50d77515
Adds sharing (residence share codes, join, user management, .casera file export/import), subscription status with feature comparison, notification preferences with bell icon, profile settings (edit info, change password, theme picker, delete account), onboarding wizard with create/join paths, enhanced dashboard with stats cards, Recharts completion chart, recent activity feed, and task report PDF download. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// Base API client for Casera web app
|
|
// ---------------------------------------------------------------------------
|
|
// All client-side requests go through Next.js API route handlers (proxy).
|
|
// The proxy reads the httpOnly `casera-token` cookie and forwards it to the
|
|
// Go API as an Authorization header. This avoids exposing the token to JS.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const API_BASE_URL =
|
|
process.env.NEXT_PUBLIC_API_URL || 'https://mycrib.treytartt.com/api';
|
|
|
|
/**
|
|
* Server-only base URL. Falls back to the public one so that server
|
|
* components / route handlers can reach the Go API directly.
|
|
*/
|
|
const SERVER_API_URL = process.env.API_URL || API_BASE_URL;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Error class
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
message: string,
|
|
public details?: Record<string, string[]>,
|
|
) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Client-side fetcher (calls Next.js /api/proxy/... routes)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Client-side authenticated fetch. Calls the Next.js catch-all proxy which
|
|
* attaches the auth token from the httpOnly cookie before forwarding to Go.
|
|
*
|
|
* @param path API path *without* the `/api` prefix, e.g. `/tasks/`
|
|
* @param options Standard RequestInit overrides
|
|
*/
|
|
export async function apiFetch<T>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
): Promise<T> {
|
|
// Ensure trailing slash (Go API requires it)
|
|
const normalized = path.endsWith('/') ? path : `${path}/`;
|
|
const url = `/api/proxy${normalized}`;
|
|
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'X-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
...(options.headers as Record<string, string>),
|
|
};
|
|
|
|
// If the caller explicitly set Content-Type to something else (e.g. for
|
|
// FormData uploads), remove our default so the browser can set the
|
|
// multipart boundary automatically.
|
|
if (options.body instanceof FormData) {
|
|
delete headers['Content-Type'];
|
|
}
|
|
|
|
const res = await fetch(url, { ...options, headers });
|
|
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
throw new ApiError(
|
|
res.status,
|
|
body.message || body.error || 'Request failed',
|
|
body.details,
|
|
);
|
|
}
|
|
|
|
// Handle 204 No Content
|
|
if (res.status === 204) return undefined as T;
|
|
|
|
return res.json();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Server-side fetcher (for server components & route handlers)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Server-side fetch that reads the auth token from the `casera-token` cookie
|
|
* and calls the Go API directly (no proxy hop).
|
|
*
|
|
* Only use this inside:
|
|
* - `app/api/.../route.ts` handlers
|
|
* - Server Components
|
|
* - Server Actions
|
|
*/
|
|
export async function serverFetch<T>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
): Promise<T> {
|
|
// Dynamic import so this module can still be imported from client bundles
|
|
// (the function itself should only be *called* on the server).
|
|
const { cookies } = await import('next/headers');
|
|
const cookieStore = await cookies();
|
|
const token = cookieStore.get('casera-token')?.value;
|
|
|
|
const normalized = path.endsWith('/') ? path : `${path}/`;
|
|
const url = `${SERVER_API_URL}${normalized}`;
|
|
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'X-Timezone': 'UTC', // Server-side default; client overrides via proxy
|
|
...(options.headers as Record<string, string>),
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Token ${token}`;
|
|
}
|
|
|
|
if (options.body instanceof FormData) {
|
|
delete headers['Content-Type'];
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
// Disable Next.js fetch cache for mutations; callers can override.
|
|
cache: options.cache ?? 'no-store',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
throw new ApiError(
|
|
res.status,
|
|
body.message || body.error || 'Request failed',
|
|
body.details,
|
|
);
|
|
}
|
|
|
|
if (res.status === 204) return undefined as T;
|
|
|
|
return res.json();
|
|
}
|