fix(web): keep the settings dialog one size and move billing into it (BUG-554)

The four account panes now share a fixed frame, chart profiles open as list then detail, and membership lives in the homepage dialog instead of a separate page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 18:28:32 +08:00
parent 40bd0e9372
commit dc6598d7e4
48 changed files with 1962 additions and 1411 deletions
+18
View File
@@ -0,0 +1,18 @@
export type ChartLibraryView =
| { readonly kind: "list" }
| { readonly kind: "self" }
| { readonly kind: "other"; readonly id: string }
| { readonly kind: "add" };
export const CHART_LIBRARY_LIST_VIEW: ChartLibraryView = { kind: "list" };
export function synastryHistoryForPartner<T extends {
readonly partnerName: string;
readonly partnerChartId?: string;
}>(history: readonly T[], partner: { readonly id: string; readonly name: string }) {
return history.filter((item) => (
item.partnerChartId
? item.partnerChartId === partner.id
: item.partnerName === partner.name
));
}
+24 -4
View File
@@ -50,6 +50,7 @@ export type ChartLibraryApiRecord = {
export type SynastryReportCard = {
id: string;
partnerName: string;
partnerChartId?: string;
score?: number;
maxScore?: number;
assessment?: string;
@@ -132,7 +133,24 @@ export type Account = {
profile: unknown;
};
export type OnboardingStep = "name" | "birth" | "place" | "rectification";
export type AccountDialog = "profile" | "chart-library" | "general" | "logout";
export type AccountDialog = "profile" | "chart-library" | "billing" | "general" | "logout";
export type BillingPaneTab = "membership" | "credits" | "redeem" | "orders";
export type OpenAccountDialogOptions = {
returnTarget?: HTMLButtonElement | null;
source?: string;
plan?: "trial" | "monthly" | "yearly";
tab?: BillingPaneTab;
};
export function accountDialogOptions(
value?: HTMLButtonElement | null | OpenAccountDialogOptions,
): OpenAccountDialogOptions {
if (value == null) return {};
if (typeof value === "object" && "nodeType" in value) {
return { returnTarget: value };
}
return value;
}
export type DailyStarlanguageCard = { trend: string; action: string; caution: string };
export type DailyStarlanguageApiResponse = {
status?: "ok" | "unavailable" | "unauthenticated";
@@ -185,14 +203,16 @@ export const themes = defaultGuidedJyotishTopics;
export const accountDialogTitles = {
profile: "个人资料",
"chart-library": "星盘资料",
billing: "账户与点数",
general: "通用设置",
logout: "退出登录?",
} as const satisfies Record<AccountDialog, string>;
export const accountDialogClasses = {
profile: "profile-modal",
"chart-library": "chart-library-modal",
general: "general-modal",
profile: "settings-modal",
"chart-library": "settings-modal",
billing: "settings-modal",
general: "settings-modal",
logout: "logout-modal",
} as const satisfies Record<AccountDialog, string>;
+8 -3
View File
@@ -27,11 +27,16 @@ export type MembershipProduct = {
};
export type MembershipPlanAlias = "trial" | "monthly" | "yearly";
export type BillingPaneTab = "membership" | "credits" | "redeem" | "orders";
export function membershipHref(source: string, options: { plan?: MembershipPlanAlias } = {}) {
const params = new URLSearchParams({ source });
export function membershipHref(
source: string,
options: { plan?: MembershipPlanAlias; tab?: BillingPaneTab } = {},
) {
const params = new URLSearchParams({ settings: "billing", source });
if (options.plan) params.set("plan", options.plan);
return `/membership?${params.toString()}`;
if (options.tab) params.set("tab", options.tab);
return `/?${params.toString()}`;
}
export function planDisplayLabel(alias: MembershipPlanAlias) {
+40
View File
@@ -0,0 +1,40 @@
import type { BillingPaneTab, MembershipPlanAlias } from "@/lib/membership";
export const SETTINGS_QUERY_KEY = "settings";
export type SettingsQuery = {
readonly pane: "billing";
readonly source: string | null;
readonly plan: MembershipPlanAlias | null;
readonly tab: BillingPaneTab | null;
};
const PLAN_ALIASES: ReadonlySet<string> = new Set(["trial", "monthly", "yearly"]);
const BILLING_TABS: ReadonlySet<string> = new Set(["membership", "credits", "redeem", "orders"]);
function searchParams(search: string) {
return new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
}
export function parseSettingsQuery(search: string): SettingsQuery | null {
const params = searchParams(search);
if (params.get(SETTINGS_QUERY_KEY) !== "billing") return null;
const planRaw = params.get("plan");
const tabRaw = params.get("tab");
return {
pane: "billing",
source: params.get("source"),
plan: planRaw && PLAN_ALIASES.has(planRaw) ? planRaw as MembershipPlanAlias : null,
tab: tabRaw && BILLING_TABS.has(tabRaw) ? tabRaw as BillingPaneTab : null,
};
}
export function stripSettingsQuery(search: string, pathname = "/") {
const params = searchParams(search);
params.delete(SETTINGS_QUERY_KEY);
params.delete("source");
params.delete("plan");
params.delete("tab");
const query = params.toString();
return query ? `${pathname}?${query}` : pathname || "/";
}