feat(payment): sync plans and add coins rules
This commit is contained in:
@@ -1,24 +1,230 @@
|
||||
/* ============================================================
|
||||
* 侧边栏主屏幕(浅色主题)
|
||||
* ============================================================ */
|
||||
.shell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-page-background, #ffffff);
|
||||
color: var(--color-auth-text-primary, #333333);
|
||||
gap: 14px;
|
||||
overflow: hidden;
|
||||
min-height: 100dvh;
|
||||
padding: 0 var(--page-padding-x, 28px) var(--spacing-xxl, 24px);
|
||||
box-sizing: border-box;
|
||||
padding: 0 var(--page-padding-x, 28px) 28px;
|
||||
background:
|
||||
radial-gradient(circle at 18% 6%, rgba(255, 206, 160, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 92% 0%, rgba(246, 87, 160, 0.22), transparent 30%),
|
||||
linear-gradient(180deg, #fcf3f4 0%, #fffafa 44%, #ffffff 100%);
|
||||
color: #191316;
|
||||
}
|
||||
|
||||
.bgOrbOne,
|
||||
.bgOrbTwo {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
border-radius: 999px;
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.bgOrbOne {
|
||||
top: 86px;
|
||||
right: -72px;
|
||||
width: 168px;
|
||||
height: 168px;
|
||||
background: rgba(246, 87, 160, 0.11);
|
||||
}
|
||||
|
||||
.bgOrbTwo {
|
||||
bottom: 56px;
|
||||
left: -88px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: rgba(248, 184, 62, 0.1);
|
||||
}
|
||||
|
||||
.topBar,
|
||||
.hero,
|
||||
.userSlot,
|
||||
.cardSlot,
|
||||
.settingsSlot {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.topBar {
|
||||
margin: 4px 0 -2px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 12px 2px 6px;
|
||||
}
|
||||
|
||||
.kicker {
|
||||
margin: 0;
|
||||
color: #f657a0;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 8px 0 0;
|
||||
color: #171114;
|
||||
font-size: 34px;
|
||||
font-weight: 920;
|
||||
letter-spacing: -1px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
max-width: 320px;
|
||||
margin: 10px 0 0;
|
||||
color: #75636a;
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.userSlot {
|
||||
padding: var(--sidebar-user-padding-y, 20px) 0;
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(25, 19, 22, 0.06);
|
||||
border-radius: 26px;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
box-shadow: 0 16px 40px rgba(55, 36, 44, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.cardSlot {
|
||||
padding: var(--sidebar-card-gap-y, 8px) 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settingsSlot {
|
||||
padding: var(--spacing-lg, 16px) 0 0;
|
||||
margin-top: 2px;
|
||||
padding: 2px 0 0;
|
||||
}
|
||||
|
||||
.settingsLabel {
|
||||
margin: 0 0 9px;
|
||||
padding: 0 4px;
|
||||
color: #817076;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.logoutCard {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(25, 19, 22, 0.06);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: #171114;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
box-shadow: 0 12px 30px rgba(55, 36, 44, 0.06);
|
||||
transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.16s ease;
|
||||
}
|
||||
|
||||
.logoutCard:hover {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16px 34px rgba(55, 36, 44, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.logoutCard:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.logoutCard:focus-visible {
|
||||
outline: 2px solid #f657a0;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.logoutIcon {
|
||||
display: inline-flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 15px;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
color: #ef4d64;
|
||||
}
|
||||
|
||||
.logoutText {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.logoutTitle {
|
||||
color: #ef4d64;
|
||||
font-size: 16px;
|
||||
font-weight: 760;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.logoutSubtitle {
|
||||
color: #817076;
|
||||
font-size: 13px;
|
||||
font-weight: 620;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.revealOne,
|
||||
.revealTwo,
|
||||
.revealThree {
|
||||
animation: revealUp 0.42s ease both;
|
||||
}
|
||||
|
||||
.revealOne {
|
||||
animation-delay: 40ms;
|
||||
}
|
||||
|
||||
.revealTwo {
|
||||
animation-delay: 95ms;
|
||||
}
|
||||
|
||||
.revealThree {
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
|
||||
@keyframes revealUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.shell {
|
||||
gap: 12px;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.userSlot {
|
||||
padding: 16px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import styles from "./sidebar-wallet-card.module.css";
|
||||
export interface SidebarWalletCardProps {
|
||||
creditBalance: number;
|
||||
onTopUp: () => void;
|
||||
onRulesClick: () => void;
|
||||
}
|
||||
|
||||
const formatCoins = (value: number): string =>
|
||||
@@ -15,6 +16,7 @@ const formatCoins = (value: number): string =>
|
||||
export function SidebarWalletCard({
|
||||
creditBalance,
|
||||
onTopUp,
|
||||
onRulesClick,
|
||||
}: SidebarWalletCardProps) {
|
||||
return (
|
||||
<section className={styles.card} aria-label="My wallet">
|
||||
@@ -31,7 +33,11 @@ export function SidebarWalletCard({
|
||||
<span className={styles.balanceUnit}> coins</span>
|
||||
</p>
|
||||
|
||||
<button type="button" className={styles.rulesButton}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.rulesButton}
|
||||
onClick={onRulesClick}
|
||||
>
|
||||
Coins 消耗规则
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { LogOut } from "lucide-react";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { MobileShell, SettingsSection } from "@/app/_components/core";
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||
@@ -20,6 +21,12 @@ import styles from "./components/sidebar-screen.module.css";
|
||||
const FALLBACK_USERNAME = "User name";
|
||||
const VIP_SUBSCRIPTION_URL = `${ROUTES.subscription}?type=vip`;
|
||||
|
||||
const STATUS_LABELS: Record<SidebarUserState, string> = {
|
||||
guest: "Sign in to keep your coins and unlock more features.",
|
||||
member: "Manage your wallet, subscription and account settings.",
|
||||
vip: "Your VIP benefits are active. Enjoy the full experience.",
|
||||
};
|
||||
|
||||
/**
|
||||
* 侧边栏主屏幕
|
||||
*
|
||||
@@ -58,12 +65,22 @@ export function SidebarScreen() {
|
||||
const avatarUrl = user.avatarUrl ?? user.currentUser?.avatarUrl ?? null;
|
||||
|
||||
return (
|
||||
<MobileShell>
|
||||
<MobileShell background="#fcf3f4">
|
||||
<div className={styles.shell}>
|
||||
<BackBar />
|
||||
<div className={styles.bgOrbOne} aria-hidden="true" />
|
||||
<div className={styles.bgOrbTwo} aria-hidden="true" />
|
||||
|
||||
{/* 用户信息区:三种状态 */}
|
||||
<section className={styles.userSlot}>
|
||||
<div className={styles.topBar}>
|
||||
<BackBar />
|
||||
</div>
|
||||
|
||||
<section className={styles.hero}>
|
||||
<p className={styles.kicker}>Cozsweet</p>
|
||||
<h1 className={styles.title}>Account</h1>
|
||||
<p className={styles.subtitle}>{STATUS_LABELS[sidebarState]}</p>
|
||||
</section>
|
||||
|
||||
<section className={`${styles.userSlot} ${styles.revealOne}`}>
|
||||
<UserHeader
|
||||
state={sidebarState}
|
||||
name={name}
|
||||
@@ -73,10 +90,11 @@ export function SidebarScreen() {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className={styles.cardSlot}>
|
||||
<section className={`${styles.cardSlot} ${styles.revealTwo}`}>
|
||||
<SidebarWalletCard
|
||||
creditBalance={user.creditBalance}
|
||||
onTopUp={() => router.push(VIP_SUBSCRIPTION_URL)}
|
||||
onRulesClick={() => router.push(ROUTES.coinsRules)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -91,18 +109,23 @@ export function SidebarScreen() {
|
||||
*/}
|
||||
|
||||
{sidebarState !== "guest" ? (
|
||||
<section className={styles.settingsSlot}>
|
||||
<SettingsSection
|
||||
title="Other"
|
||||
items={[
|
||||
{
|
||||
id: "logout",
|
||||
title: "Log out",
|
||||
destructive: true,
|
||||
onClick: handleLogoutClick,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<section className={`${styles.settingsSlot} ${styles.revealThree}`}>
|
||||
<p className={styles.settingsLabel}>Other</p>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.logoutCard}
|
||||
onClick={handleLogoutClick}
|
||||
>
|
||||
<span className={styles.logoutIcon} aria-hidden="true">
|
||||
<LogOut size={19} strokeWidth={2.2} />
|
||||
</span>
|
||||
<span className={styles.logoutText}>
|
||||
<span className={styles.logoutTitle}>Log out</span>
|
||||
<span className={styles.logoutSubtitle}>
|
||||
End the current session safely
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user