Add complete production-ready Ysterbaard PocketBase registry browser frontend: - Next.js 14 App Router with TypeScript, Tailwind CSS, and TanStack React Query - Secure server-side PocketBase API proxy to avoid exposing superuser tokens - Read-only directors and companies registry browse pages with search, filtering, and pagination - Client onboarding workflow for creating and managing client submissions - Docker container configuration for easy deployment - All supporting UI components, utility functions, and configuration files
104 lines
3.9 KiB
TypeScript
104 lines
3.9 KiB
TypeScript
// lib/pb.ts
|
|
// SERVER-ONLY PocketBase client.
|
|
import "server-only";
|
|
|
|
const BASE_URL = process.env.PB_URL;
|
|
const TOKEN = process.env.PB_TOKEN;
|
|
|
|
if (!BASE_URL) throw new Error("PB_URL is not set.");
|
|
if (!TOKEN || TOKEN === "PASTE_SUPERUSER_JWT_HERE") throw new Error("PB_TOKEN missing");
|
|
|
|
export interface PBList<T> { page: number; perPage: number; totalItems: number; totalPages: number; items: T[]; }
|
|
export interface PBRecord { id: string; collectionId: string; collectionName: string; [key: string]: unknown; }
|
|
|
|
function buildUrl(path: string, query: Record<string, string | number | undefined> = {}): string {
|
|
const params = new URLSearchParams();
|
|
for (const [k, v] of Object.entries(query)) {
|
|
if (v !== undefined && v !== null && v !== "") params.set(k, String(v));
|
|
}
|
|
const qs = params.toString();
|
|
return `${BASE_URL}${path}${qs ? `?${qs}` : ""}`;
|
|
}
|
|
|
|
export async function pbFetch<T>(path: string, query: Record<string, string | number | undefined> = {}): Promise<T> {
|
|
const url = buildUrl(path, query);
|
|
const res = await fetch(url, {
|
|
headers: { Authorization: TOKEN as string },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on ${path}: ${body.slice(0, 200)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function createRecordMultipart<T extends PBRecord>(collection: string, fields: Record<string, string>, files: { field: string; file: File }[] = []): Promise<T> {
|
|
const form = new FormData();
|
|
for (const [k, v] of Object.entries(fields)) form.set(k, v);
|
|
for (const { field, file } of files) form.append(field, file, file.name);
|
|
const url = `${BASE_URL}/api/collections/${collection}/records`;
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { Authorization: TOKEN as string },
|
|
body: form,
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on POST ${collection}: ${body.slice(0, 300)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function updateRecord<T extends PBRecord>(collection: string, id: string, fields: Record<string, unknown>): Promise<T> {
|
|
const url = `${BASE_URL}/api/collections/${collection}/records/${encodeURIComponent(id)}`;
|
|
const res = await fetch(url, {
|
|
method: "PATCH",
|
|
headers: { Authorization: TOKEN as string, "Content-Type": "application/json" },
|
|
body: JSON.stringify(fields),
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on PATCH ${collection}/${id}: ${body.slice(0, 300)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function fetchCollection<T extends PBRecord>(collection: string, params: { page?: number; perPage?: number; sort?: string; filter?: string; expand?: string; fields?: string } = {}): Promise<PBList<T>> {
|
|
return pbFetch<PBList<T>>(`/api/collections/${collection}/records`, {
|
|
page: params.page ?? 1,
|
|
perPage: params.perPage ?? 25,
|
|
sort: params.sort,
|
|
filter: params.filter,
|
|
expand: params.expand,
|
|
fields: params.fields,
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* JSON create — for collections without file fields.
|
|
*/
|
|
export async function createRecord<T extends PBRecord>(
|
|
collection: string,
|
|
fields: Record<string, unknown>,
|
|
): Promise<T> {
|
|
const url = `${BASE_URL}/api/collections/${collection}/records`;
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { Authorization: TOKEN as string, "Content-Type": "application/json" },
|
|
body: JSON.stringify(fields),
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on POST ${collection}: ${body.slice(0, 300)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function fetchRecord<T = unknown>(collection: string, id: string, expand?: string): Promise<T> {
|
|
return pbFetch<T>(`/api/collections/${collection}/records/${encodeURIComponent(id)}`, { expand });
|
|
}
|