214 lines
6.6 KiB
TypeScript
214 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import {
|
|
Gift,
|
|
ImageIcon,
|
|
MessageCircle,
|
|
Mic2,
|
|
Sparkles,
|
|
} from "lucide-react";
|
|
import { FaCoins } from "react-icons/fa6";
|
|
|
|
import { BackButton } from "@/app/_components";
|
|
import { MobileShell } from "@/app/_components/core";
|
|
import {
|
|
resolveGlobalRouteContext,
|
|
type GlobalReturnToValue,
|
|
} from "@/router/global-route-context";
|
|
import { useAuthState } from "@/stores/auth/auth-context";
|
|
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
|
|
|
import styles from "./coins-rules-screen.module.css";
|
|
|
|
interface CoinRuleItem {
|
|
title: string;
|
|
cost: string;
|
|
free?: string;
|
|
icon: typeof MessageCircle;
|
|
}
|
|
|
|
const formatDailyFreeMessages = (count: number, label: string): string =>
|
|
`${Math.max(0, Math.trunc(count))} free ${label} daily`;
|
|
|
|
const formatCoinCost = (cost: number | null, unit: string): string =>
|
|
cost === null
|
|
? "Current rate shown at unlock"
|
|
: `${Math.max(0, Math.trunc(cost))} coins / ${unit}`;
|
|
|
|
const createCoinRules = (
|
|
dailyFreeChatLimit: number | null,
|
|
dailyFreePrivateLimit: number | null,
|
|
costs: {
|
|
normalMessage: number | null;
|
|
privateMessage: number | null;
|
|
voiceMessage: number | null;
|
|
photo: number | null;
|
|
privateAlbum10: number | null;
|
|
privateAlbum20: number | null;
|
|
},
|
|
): readonly CoinRuleItem[] => [
|
|
{
|
|
title: "Standard Chat",
|
|
cost: formatCoinCost(costs.normalMessage, "message"),
|
|
free:
|
|
dailyFreeChatLimit === null
|
|
? "Current daily allowance loads from your account"
|
|
: formatDailyFreeMessages(dailyFreeChatLimit, "messages"),
|
|
icon: MessageCircle,
|
|
},
|
|
{
|
|
title: "Private Chat",
|
|
cost: formatCoinCost(costs.privateMessage, "message"),
|
|
free:
|
|
dailyFreePrivateLimit === null
|
|
? "Current daily allowance loads from your account"
|
|
: formatDailyFreeMessages(dailyFreePrivateLimit, "private messages"),
|
|
icon: Sparkles,
|
|
},
|
|
{
|
|
title: "Voice Message",
|
|
cost: formatCoinCost(costs.voiceMessage, "message"),
|
|
icon: Mic2,
|
|
},
|
|
// {
|
|
// title: "Voice Call",
|
|
// cost: "50 coins / call",
|
|
// icon: Phone,
|
|
// },
|
|
{
|
|
title: "Photo",
|
|
cost: formatCoinCost(costs.photo, "photo"),
|
|
icon: ImageIcon,
|
|
},
|
|
{
|
|
title: "Private Album",
|
|
cost:
|
|
costs.privateAlbum10 === null
|
|
? "Current price shown in Private Zone"
|
|
: `10 photos / ${Math.max(0, Math.trunc(costs.privateAlbum10))} coins`,
|
|
icon: ImageIcon,
|
|
},
|
|
{
|
|
title: "Private Album",
|
|
cost:
|
|
costs.privateAlbum20 === null
|
|
? "Current price shown in Private Zone"
|
|
: `20 photos / ${Math.max(0, Math.trunc(costs.privateAlbum20))} coins`,
|
|
icon: ImageIcon,
|
|
},
|
|
] as const;
|
|
|
|
export interface CoinsRulesScreenProps {
|
|
returnTo?: GlobalReturnToValue;
|
|
}
|
|
|
|
export function CoinsRulesScreen({ returnTo }: CoinsRulesScreenProps) {
|
|
const navigation = resolveGlobalRouteContext(returnTo);
|
|
const authState = useAuthState();
|
|
const userState = useUserState();
|
|
const userDispatch = useUserDispatch();
|
|
const entitlements = userState.entitlements;
|
|
const dailyFreeChatLimit =
|
|
entitlements?.quotas.normalChatFreeDaily ??
|
|
userState.currentUser?.dailyFreeChatLimit ??
|
|
null;
|
|
const dailyFreePrivateLimit =
|
|
entitlements?.quotas.privateUnlockFreeDaily ??
|
|
userState.currentUser?.dailyFreePrivateLimit ??
|
|
null;
|
|
const standardChatLabel =
|
|
dailyFreeChatLimit === null
|
|
? "the current daily allowance shown for your account"
|
|
: formatDailyFreeMessages(dailyFreeChatLimit, "messages");
|
|
const privateChatLabel =
|
|
dailyFreePrivateLimit === null
|
|
? "the current private allowance shown for your account"
|
|
: formatDailyFreeMessages(dailyFreePrivateLimit, "private messages");
|
|
const coinRules = createCoinRules(
|
|
dailyFreeChatLimit,
|
|
dailyFreePrivateLimit,
|
|
{
|
|
normalMessage: entitlements?.costs.normal_message ?? null,
|
|
privateMessage: entitlements?.costs.private_message ?? null,
|
|
voiceMessage: entitlements?.costs.voice_message ?? null,
|
|
photo: entitlements?.costs.photo ?? null,
|
|
privateAlbum10: entitlements?.costs.private_album_10 ?? null,
|
|
privateAlbum20: entitlements?.costs.private_album_20 ?? null,
|
|
},
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!authState.hasInitialized || authState.isLoading) return;
|
|
if (authState.loginStatus === "notLoggedIn") return;
|
|
userDispatch({ type: "UserFetch" });
|
|
}, [
|
|
authState.hasInitialized,
|
|
authState.isLoading,
|
|
authState.loginStatus,
|
|
userDispatch,
|
|
]);
|
|
|
|
return (
|
|
<MobileShell background="#fcf3f4">
|
|
<main className={styles.screen}>
|
|
<header className={styles.header}>
|
|
<BackButton
|
|
href={navigation.profileUrl}
|
|
variant="soft"
|
|
aria-label="Back to profile"
|
|
/>
|
|
|
|
<div className={styles.hero}>
|
|
<span className={styles.heroIcon} aria-hidden="true">
|
|
<FaCoins />
|
|
</span>
|
|
<p className={styles.eyebrow}>Coins Guide</p>
|
|
<h1 className={styles.title}>Coin Usage Rules</h1>
|
|
<p className={styles.subtitle}>
|
|
See how many coins each interaction costs before you top up.
|
|
</p>
|
|
</div>
|
|
</header>
|
|
|
|
<section className={styles.freeCard} aria-label="Free quota">
|
|
<div className={styles.freeIcon} aria-hidden="true">
|
|
<Gift strokeWidth={2.3} />
|
|
</div>
|
|
<div>
|
|
<h2 className={styles.freeTitle}>Free Allowance</h2>
|
|
<p className={styles.freeText}>
|
|
Standard chat includes {standardChatLabel}. Private chat includes{" "}
|
|
{privateChatLabel}.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section className={styles.ruleList} aria-label="Coins usage rules">
|
|
{coinRules.map((rule, index) => {
|
|
const Icon = rule.icon;
|
|
return (
|
|
<article
|
|
key={`${rule.title}-${rule.cost}`}
|
|
className={styles.ruleCard}
|
|
style={{ animationDelay: `${index * 45}ms` }}
|
|
>
|
|
<span className={styles.ruleIcon} aria-hidden="true">
|
|
<Icon strokeWidth={2.35} />
|
|
</span>
|
|
<div className={styles.ruleContent}>
|
|
<h2 className={styles.ruleTitle}>{rule.title}</h2>
|
|
{rule.free ? (
|
|
<p className={styles.freeBadge}>{rule.free}</p>
|
|
) : null}
|
|
</div>
|
|
<p className={styles.cost}>{rule.cost}</p>
|
|
</article>
|
|
);
|
|
})}
|
|
</section>
|
|
</main>
|
|
</MobileShell>
|
|
);
|
|
}
|