Files
cozsweet-frontend-nextjs/src/app/coins-rules/coins-rules-screen.tsx
T

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>
);
}