feat(characters): use local character catalog

This commit is contained in:
2026-07-17 16:03:18 +08:00
parent a210a98d98
commit b3ebd5cf3b
96 changed files with 1023 additions and 522 deletions
+6 -4
View File
@@ -1,7 +1,9 @@
import type { ReactNode } from "react";
import { PaymentRouteProvider } from "@/providers/payment-route-provider";
export default function TipLayout({ children }: { children: ReactNode }) {
return <PaymentRouteProvider>{children}</PaymentRouteProvider>;
export default function LegacyTipLayout({
children,
}: {
children: ReactNode;
}) {
return children;
}
+13 -26
View File
@@ -1,34 +1,21 @@
import {
getFirstPaymentSearchParam,
parsePaymentReturnSearchParams,
type PaymentSearchParams,
} from "@/lib/payment/payment_search_params";
import {
DEFAULT_TIP_COFFEE_TYPE,
resolveTipCoffeeType,
TIP_COFFEE_TYPE_PARAM,
} from "@/lib/tip/tip_coffee";
import { redirect } from "next/navigation";
import { TipScreen } from "./tip-screen";
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
import {
appendRouteSearchParams,
getCharacterRoutes,
type RouteSearchParams,
} from "@/router/routes";
export default async function TipPage({
searchParams,
}: {
searchParams: Promise<PaymentSearchParams>;
searchParams: Promise<RouteSearchParams>;
}) {
const query = await searchParams;
const paymentReturn = parsePaymentReturnSearchParams(query);
const coffeeType =
resolveTipCoffeeType(
getFirstPaymentSearchParam(query[TIP_COFFEE_TYPE_PARAM]),
) ??
DEFAULT_TIP_COFFEE_TYPE;
return (
<TipScreen
coffeeType={coffeeType}
shouldResumePendingOrder={paymentReturn.shouldResumePendingOrder}
initialPayChannel={paymentReturn.initialPayChannel}
/>
redirect(
appendRouteSearchParams(
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).tip,
await searchParams,
),
);
}
+3
View File
@@ -3,6 +3,7 @@
import { usePaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow";
import { PaymentLaunchDialogs } from "@/app/_components/payment/payment-launch-dialogs";
import type { TipCoffeeType } from "@/lib/tip/tip_coffee";
import { useActiveCharacter } from "@/providers/character-provider";
import {
usePaymentDispatch,
usePaymentState,
@@ -28,6 +29,7 @@ export function TipCheckoutButton({
onOrder,
returnPath,
}: TipCheckoutButtonProps) {
const character = useActiveCharacter();
const payment = usePaymentState();
const paymentDispatch = usePaymentDispatch();
const paymentLaunch = usePaymentLaunchFlow({
@@ -37,6 +39,7 @@ export function TipCheckoutButton({
paymentDispatch,
subscriptionType: "tip",
tipCoffeeType: coffeeType,
characterSlug: character.slug,
});
const isLoading =
+1 -1
View File
@@ -26,7 +26,7 @@
height: 330px;
background:
linear-gradient(180deg, rgba(255, 242, 232, 0.3), #fff2e8 96%),
url("/images/cover/elio.png") center 18% / cover no-repeat;
var(--tip-cover-image) center 18% / cover no-repeat;
opacity: 0.36;
filter: saturate(0.95) blur(0.2px);
}
+27 -7
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState, type CSSProperties } from "react";
import Image from "next/image";
import { ArrowLeft, Heart, Sparkles } from "lucide-react";
@@ -21,6 +21,10 @@ import {
type TipCoffeeType,
} from "@/lib/tip/tip_coffee";
import { useAppNavigator } from "@/router/use-app-navigator";
import {
useActiveCharacter,
useActiveCharacterRoutes,
} from "@/providers/character-provider";
import { useAuthState } from "@/stores/auth/auth-context";
import { TipCheckoutButton } from "./tip-checkout-button";
@@ -52,11 +56,16 @@ export function TipScreen({
initialPayChannel = null,
}: TipScreenProps) {
const navigator = useAppNavigator();
const character = useActiveCharacter();
const characterRoutes = useActiveCharacterRoutes();
const authState = useAuthState();
const [selectedCoffeeType, setSelectedCoffeeType] =
useState<TipCoffeeType>(coffeeType);
const coffeeOption = getTipCoffeeOption(selectedCoffeeType);
const returnPath = buildTipCoffeePath(selectedCoffeeType);
const returnPath = buildTipCoffeePath(
selectedCoffeeType,
characterRoutes.tip,
);
const resolvedInitialPayChannel =
initialPayChannel ?? navigator.getDefaultPayChannel();
const { payment, paymentDispatch } = usePaymentRouteFlow({
@@ -155,7 +164,10 @@ export function TipScreen({
}
if (!canCreateOrder) return;
paymentDispatch({ type: "PaymentCreateOrderSubmitted" });
paymentDispatch({
type: "PaymentCreateOrderSubmitted",
recipientCharacterId: character.id,
});
};
const handleCoffeeTypeChange = (type: TipCoffeeType) => {
@@ -182,7 +194,14 @@ export function TipScreen({
return (
<MobileShell background="#fff5ed">
<main className={styles.shell}>
<main
className={styles.shell}
style={
{
"--tip-cover-image": `url("${character.assets.cover}")`,
} as CSSProperties
}
>
<div className={styles.bgImage} aria-hidden="true" />
<div className={styles.bgGlowOne} aria-hidden="true" />
<div className={styles.bgGlowTwo} aria-hidden="true" />
@@ -198,13 +217,14 @@ export function TipScreen({
>
<ArrowLeft size={19} aria-hidden="true" />
</button>
<span className={styles.headerPill}>Tip Elio</span>
<span className={styles.headerPill}>{character.copy.tipHeader}</span>
</header>
<section className={styles.hero} aria-labelledby="tip-title">
<div className={styles.avatarRing}>
<CharacterAvatar
alt="Elio Silvestri"
src={character.assets.avatar}
alt={character.displayName}
size="100%"
imageSize={88}
priority
@@ -212,7 +232,7 @@ export function TipScreen({
</div>
<p className={styles.eyebrow}>A little sweetness for today</p>
<h1 id="tip-title" className={styles.title}>
Buy Elio a coffee
{character.copy.tipTitle}
</h1>
<p className={styles.subtitle}>
Send a warm coffee tip and keep the private moments glowing.