feat: complete Phase 3 — advanced features for Casera web app
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>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user