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:
@@ -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
|
||||
));
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 || "/";
|
||||
}
|
||||
Reference in New Issue
Block a user