Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
45
src/lib/api/client.ts
Normal file
45
src/lib/api/client.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { useUIStore } from '@/stores/ui-store';
|
||||
|
||||
export interface ApiFetchOptions extends Omit<RequestInit, 'body'> {
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side fetch wrapper that attaches the `X-Port-Id` header from the
|
||||
* UI store to every request. Used by all queryFn/mutationFn callbacks.
|
||||
*/
|
||||
export async function apiFetch<T = unknown>(
|
||||
url: string,
|
||||
opts: ApiFetchOptions = {},
|
||||
): Promise<T> {
|
||||
const portId = useUIStore.getState().currentPortId;
|
||||
|
||||
const headers = new Headers(opts.headers);
|
||||
if (portId) {
|
||||
headers.set('X-Port-Id', portId);
|
||||
}
|
||||
if (opts.body !== undefined && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
...opts,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ error: res.statusText }));
|
||||
throw Object.assign(new Error(error.error ?? 'Request failed'), {
|
||||
status: res.status,
|
||||
code: error.code,
|
||||
details: error.details,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
Reference in New Issue
Block a user