These selects demonstrate production-ready integration with the backend-authoritative Reference Data Platform via the Next.js BFF /api/ proxy. Use serverSearch=falsefor static lists (countries/timezones/languages) to fetch once and filter client-side.
Browser talks to Next.js only; Next.js proxies to Django. This matches your CoreIdentity/BFF posture.
// Browser calls Next.js (same origin). Next proxies to Django.
// Catch-all BFF proxy route (this repo):
// src/app/api/[...path]/route.ts
//
// Key projects:
// - allowlisted backend prefixes only (e.g. /api/v1/)
// - never forwards client Authorization/cookies
// - injects Authorization: Bearer <access_token> from httpOnly cookie
// - refreshes via Identity /token (grant_type=refresh_token) and retries once
// Example: browser fetch
// fetch("/api/v1/meta/bootstrap")
ISO 3166-1. Persist the country code (alpha-2).
import { ApiSelect } from "@/design-system/components/ApiSelect";
export function CountriesSelect() {
return (
<ApiSelect
label="Country"
placeholder="Select country"
url="/api/v1/meta/countries/?locale=en"
serverSearch={false} // fetch once, filter client-side
queryParam={undefined}
transform={(data) => {
const list = Array.isArray(data)
? data
: (data as any)?.countries ?? (data as any)?.items ?? [];
return (list ?? []).map((c: any) => ({
value: c.code,
label: c.name,
keywords: [c.alpha3, c.numeric],
meta: c,
}));
}}
/>
);
}
Cascades off the selected country. Seeded into Postgres from GeoNames cities15000 (population >= 15k) via `seed_reference_cities`; queries never leave your infra (zero external APIs at runtime). Persist the chosen city name; CityApiSelect falls back to free-text for long-tail towns.
import { useState } from "react";
import { CityApiSelect, CountryApiSelect } from "@/components/location/LocationSelects.client";
// Cities are country-scoped and server-driven. The city list cascades off
// the selected country and is seeded into Postgres via:
// python manage.py seed_reference_cities (GeoNames cities15000)
// Best practice:
// - persist the country code (alpha-2) + the chosen city name
// - CityApiSelect falls back to free-text for long-tail / custom cities
export function LocationFields() {
const [country, setCountry] = useState("");
const [city, setCity] = useState("");
return (
<>
<CountryApiSelect
name="country"
locale="en"
value={country}
onValueChange={(next) => { setCountry(next); setCity(""); }}
/>
<CityApiSelect
name="city"
locale="en"
country={country}
value={city}
onValueChange={setCity}
/>
</>
);
}
IANA timezones. Use virtualization for smoother performance.
import { ApiSelect } from "@/design-system/components/ApiSelect";
export function TimezoneSelect() {
return (
<ApiSelect
label="Timezone"
placeholder="Select timezone"
url="/api/v1/meta/timezones/?locale=en"
serverSearch={false}
queryParam={undefined}
virtualization={{ threshold: 200 }}
transform={(data) => {
const list = Array.isArray(data)
? data
: (data as any)?.timezones ?? (data as any)?.items ?? [];
return (list ?? []).map((tz: any) => ({
value: tz.id,
label: tz.label ?? tz.id,
keywords: [tz.offset, tz.id],
meta: tz,
}));
}}
/>
);
}
ISO 639-1 with dir (rtl/ltr). Store canonical language code.
// See preview component for mapping + client-side filtering.Backend is authoritative. Persist only E.164. Treat phone validation as PII; avoid caching responses.
// Phone recommended pattern:
// 1) GET /api/v1/meta/phone-countries/ (or bootstrap) for dropdown
// 2) POST /api/v1/meta/phone/validate/ on blur/submit
// 3) Persist only E.164 returned by backend
const res = await fetch("/api/v1/meta/phone/validate/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: "0501234567", region: "SA", strict_region: true }),
});
const json = await res.json();
// json.e164 is the canonical value to store.