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:
Trey t
2026-03-03 09:31:29 -06:00
commit 5a50d77515
183 changed files with 34450 additions and 0 deletions
+141
View File
@@ -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();
}