Files
Jyotisha/frontend/src/lib/china-birth-place.ts
T
Jesse_ChenandClaude Opus 5.5 b743b16b00 perf(home): city table, sealed-holdout JSON and the onboarding shell leave the first screen (BUG-1130)
The China city table loads on demand; old code-only profiles load it before
the people list settles, so they resolve exactly as before. The gate keeps
the six holdout fields it reads, pinned to the JSON by a test. The
onboarding shell is preloaded before reveal only when the profile is
incomplete.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-01 00:31:53 +08:00

115 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { chinaLocations, type CityNode, type LocationNode, type ProvinceNode } from "../data/china-locations.ts";
import { registerChinaCountry } from "./china-locations-client.ts";
export const chinaProvinces = chinaLocations.country.provinces;
// Whoever loads the picker has the table; old profiles read it from here too.
registerChinaCountry(chinaLocations.country);
export type BirthPlaceLevels = {
provinceCode: string;
cityCode: string;
districtCode: string;
};
export type ResolvedBirthPlaceNode = {
province: ProvinceNode;
city: CityNode;
district: LocationNode | undefined;
node: LocationNode;
placeType: "province" | "city" | "district";
label: string;
latitude: number;
longitude: number;
};
/**
* Municipalities and special administrative regions repeat the province as their
* only city, so that level carries no choice and is not worth showing.
*/
export function hasVirtualCityLevel(province: ProvinceNode) {
return province.cities.length === 1 && province.cities[0].name === province.name;
}
export function findProvinceNode(code: string) {
return chinaProvinces.find((province) => province.code === code);
}
export function findCityNode(province: ProvinceNode | undefined, code: string) {
return province?.cities.find((city) => city.code === code);
}
function resolveCityNode(province: ProvinceNode, cityCode: string) {
return hasVirtualCityLevel(province) ? province.cities[0] : findCityNode(province, cityCode);
}
/** Resolves the deepest administrative node the three levels can name. */
export function resolveBirthPlaceNode(
{ provinceCode, cityCode, districtCode }: BirthPlaceLevels,
): ResolvedBirthPlaceNode | null {
const province = findProvinceNode(provinceCode);
if (!province) return null;
const city = resolveCityNode(province, cityCode);
if (!city) return null;
const district = districtCode ? city.districts.find((item) => item.code === districtCode) : undefined;
if (districtCode && !district) return null;
const node = district ?? city;
const placeType = district
? "district" as const
: city.districts.length === 0 && hasVirtualCityLevel(province)
? "province" as const
: "city" as const;
const label = [province.name, city.name, district?.name]
.filter((name, index, names): name is string => Boolean(name) && names.indexOf(name) === index)
.join(" · ");
return {
province,
city,
district,
node,
placeType,
label,
latitude: node.center[1],
longitude: node.center[0],
};
}
/**
* A saved profile stores an empty district both for cities that have none and for
* users who could only name the city, so a stored city with districts resumes as
* "city centre". Keeping that mapping stable is what makes re-syncing idempotent.
*/
export function birthPlaceDraftFromLevels({ provinceCode, cityCode, districtCode }: BirthPlaceLevels) {
const province = findProvinceNode(provinceCode);
if (!province) return { provinceCode: "", cityCode: "", districtCode: "", cityCentre: false };
const city = resolveCityNode(province, cityCode);
if (!city) return { provinceCode, cityCode: "", districtCode: "", cityCentre: false };
const district = districtCode ? city.districts.find((item) => item.code === districtCode) : undefined;
return {
provinceCode,
cityCode: city.code,
districtCode: district?.code ?? "",
cityCentre: !district && city.districts.length > 0,
};
}
export function formatBirthPlaceCoordinate(latitude: number, longitude: number) {
const northSouth = latitude >= 0 ? "北纬" : "南纬";
const eastWest = longitude >= 0 ? "东经" : "西经";
return `${eastWest} ${Math.abs(longitude).toFixed(2)}° · ${northSouth} ${Math.abs(latitude).toFixed(2)}°`;
}
export function formatBirthPlaceTimezone(timezoneId: string, timezoneOffset: number | null) {
if (timezoneOffset === null) return timezoneId;
const sign = timezoneOffset >= 0 ? "+" : "−";
const absolute = Math.abs(timezoneOffset);
const hours = Math.floor(absolute);
const minutes = Math.round((absolute - hours) * 60);
return `${timezoneId} · UTC${sign}${hours}${minutes === 0 ? "" : `:${String(minutes).padStart(2, "0")}`}`;
}