Files
Jyotisha/frontend/src/lib/people-archive-view.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

79 lines
3.5 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.
/**
* Pure display helpers for /people (TASK-people-ephemeris-ui-20260926 D2–D4).
* No new facts: every string is read from the stored profile or an existing
* label table.
*/
import { AYANAMSA_OPTIONS, resolveAyanamsa } from "@/lib/ayanamsa";
import { effectiveBirthDate } from "@/lib/effective-birth-date";
import { findCityNode, findProvinceNode } from "@/lib/china-birth-place";
import { profileBirthTimeLabel } from "@/lib/home-profile";
import type { Profile } from "@/lib/home-types";
/** D4: four existing element tokens; the person's id picks one, stably. */
export const PEOPLE_AVATAR_TONES = ["fire", "earth", "air", "water"] as const;
export type PeopleAvatarTone = (typeof PEOPLE_AVATAR_TONES)[number] | "self";
export function peopleAvatarTone(id: string, role: "self" | "other"): PeopleAvatarTone {
if (role === "self") return "self";
let hash = 0;
for (const character of id) hash = (hash * 31 + character.codePointAt(0)!) >>> 0;
return PEOPLE_AVATAR_TONES[hash % PEOPLE_AVATAR_TONES.length];
}
export function peopleDisplayName(profile: Profile, role: "self" | "other"): string {
return profile.name.trim() || (role === "self" ? "我" : "未命名资料");
}
/** 本人 shows 「我」; everyone else the first character of the name. */
export function peopleAvatarInitial(profile: Profile, role: "self" | "other"): string {
if (role === "self") return "我";
return Array.from(peopleDisplayName(profile, role))[0] ?? "?";
}
function trimCitySuffix(name: string): string {
return name.length > 2 && name.endsWith("市") ? name.slice(0, -1) : name;
}
/**
* The city for the one-line list meta. Chinese places come from the location
* codes; a free-text label keeps its first comma segment, or the city segment
* of a 「中国 · 省 · 市 · 区」 label. Empty when the place is not complete.
*/
export function peopleCityLabel(profile: Profile): string {
// The people page carries the table itself (china-birth-place imports it statically).
const city = findCityNode(findProvinceNode(profile.provinceCode), profile.cityCode);
if (city) return trimCitySuffix(city.name);
const label = profile.birthPlaceLabel.trim();
if (!label) return "";
if (label.includes("·")) {
const parts = label.split("·").map((part) => part.trim()).filter(Boolean);
const withoutCountry = parts[0] === "中国" ? parts.slice(1) : parts;
return trimCitySuffix(withoutCountry[1] ?? withoutCountry[0] ?? "");
}
const [first] = label.split(/[,,]/).map((part) => part.trim()).filter(Boolean);
return first ?? "";
}
/** D2: 「出生日期 · 排盘时间 · 城市」, missing parts dropped. */
export function peopleRowMeta(profile: Profile): string {
return [effectiveBirthDate(profile) ?? "", profileBirthTimeLabel(profile), peopleCityLabel(profile)]
.filter(Boolean)
.join(" · ");
}
/**
* D3: a rectification result was adopted or confirmed. The case id is required
* too: `readProfile` falls back to `confirmed` for any legacy row that simply
* has a time, and a hospital-record time is not a 校正结果.
*/
export function peopleTimeRectified(profile: Profile): boolean {
return (profile.birthTimeStatus === "accepted" || profile.birthTimeStatus === "confirmed")
&& profile.rectificationCaseId.trim() !== "";
}
/** D3: 「Raman(默认)」, never the stored lower-case key. */
export function peopleAyanamsaLabel(profile: Profile): string {
const value = resolveAyanamsa(profile);
return AYANAMSA_OPTIONS.find((option) => option.value === value)?.label ?? value;
}