Files
Jyotisha/frontend/src/lib/birth-location-timezone-service.ts
T
Jesse_Chen 9dc2948ab6
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
feat(onboarding): pick the birth place by level instead of by search
The single search field asked people to type a place name and then judge
which of several near-identical results was theirs, which is the one thing
they cannot verify about their own birth record. Province, city and district
are now chosen from the dataset the app already ships, so there is nothing to
type and nothing to disambiguate.

Levels that offer no choice collapse: municipalities show one level, and
prefecture cities without districts stop at the city. A district can be left
as the city centre, which is accurate enough because the chart only needs
coordinates and a timezone.

The timezone is resolved once for the chosen place rather than on every
keystroke, through a dedicated route that both this picker and the Geoapify
path share.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 18:42:54 +08:00

63 lines
2.3 KiB
TypeScript

import type {
BirthLocationTimezoneQuery,
BirthLocationTimezoneResult,
NormalizedBirthLocation,
} from "./location-contract";
type FetchLike = typeof fetch;
const localTimeStatuses = ["resolved", "not_provided", "ambiguous", "nonexistent"] as const;
function asLocalTimeStatus(value: unknown): NormalizedBirthLocation["localTimeStatus"] {
const candidate = String(value);
return (localTimeStatuses as readonly string[]).includes(candidate)
? candidate as NormalizedBirthLocation["localTimeStatus"]
: "not_provided";
}
/**
* Resolves an IANA zone and the offset that applied at the birth moment. The chart
* engine owns pre-1949 zone boundaries and historical DST, so this stays a server
* call rather than a browser tzdata lookup.
*/
export async function resolveBirthLocationTimezone(
query: BirthLocationTimezoneQuery,
options: { apiBase?: string; fetchImpl?: FetchLike } = {},
): Promise<BirthLocationTimezoneResult> {
const fetchImpl = options.fetchImpl ?? fetch;
const apiBase = options.apiBase ?? process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
let payload: Record<string, unknown>;
try {
const response = await fetchImpl(`${apiBase}/api/location/timezone`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
latitude: query.latitude,
longitude: query.longitude,
...(query.birthDate ? { birthDate: query.birthDate } : {}),
...(query.birthDate && query.birthTime ? { birthTime: query.birthTime } : {}),
}),
cache: "no-store",
});
if (!response.ok) return { status: "unavailable", reason: "timezone_service_unavailable" };
const body: unknown = await response.json();
payload = body && typeof body === "object" ? body as Record<string, unknown> : {};
} catch {
return { status: "unavailable", reason: "timezone_service_unavailable" };
}
const timezoneId = typeof payload.timezoneId === "string" ? payload.timezoneId.trim() : "";
if (payload.available !== true || !timezoneId) {
return { status: "unavailable", reason: "timezone_service_unavailable" };
}
return {
status: "ok",
timezoneId,
timezoneOffset: typeof payload.timezoneOffset === "number" ? payload.timezoneOffset : null,
timezoneSource: "iana_historical",
localTimeStatus: asLocalTimeStatus(payload.localTimeStatus),
};
}