Files
visit.waterschurch.org/src/lib/api.ts
T

80 lines
2.5 KiB
TypeScript
Raw Normal View History

import { site } from "@/config/site";
import type { PlanVisitData } from "@/lib/schema";
export interface LocateResponse {
ok: boolean;
approximate?: { lat: number; lng: number; city?: string; region?: string; confidence: "low" | "medium" };
reason?: string;
}
export interface GeocodeResponse {
ok: boolean;
location?: { lat: number; lng: number; city?: string; state?: string };
error?: string;
}
export type IntegrationStatus = "queued" | "sent" | "skipped" | "failed" | "mock";
export interface SubmitResponse {
ok: boolean;
submissionId?: string;
duplicate?: boolean;
email?: { status: IntegrationStatus };
sms?: { status: IntegrationStatus };
error?: string;
fieldErrors?: Record<string, string>;
retryAfter?: number;
}
const DEFAULT_TIMEOUT = 10_000;
export class ApiError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
async function request<T>(path: string, init: RequestInit & { timeoutMs?: number } = {}): Promise<T> {
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), init.timeoutMs ?? DEFAULT_TIMEOUT);
try {
const res = await fetch(`${site.apiBase}${path}`, {
...init,
signal: controller.signal,
headers: { Accept: "application/json", "X-Requested-With": "visit-page", ...(init.headers ?? {}) },
credentials: "same-origin",
});
const text = await res.text();
let data: unknown = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
throw new ApiError(`Bad response (${res.status})`, res.status);
}
if (!res.ok && (!data || typeof data !== "object")) throw new ApiError(`Request failed (${res.status})`, res.status);
return data as T;
} finally {
clearTimeout(t);
}
}
/** Approximate, server-side IP geolocation. Never blocks the page. */
export function locateApprox(): Promise<LocateResponse> {
return request<LocateResponse>("/locate.php", { method: "GET", timeoutMs: 4000 });
}
export function geocodeZip(zip: string): Promise<GeocodeResponse> {
return request<GeocodeResponse>(`/geocode.php?zip=${encodeURIComponent(zip)}`, { method: "GET", timeoutMs: 8000 });
}
export function submitPlanVisit(data: PlanVisitData): Promise<SubmitResponse> {
return request<SubmitResponse>("/plan-visit.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
timeoutMs: 20_000,
});
}