feat(sidebar): show user info with three-state UI (guest/member/vip)
Sidebar screen now primarily displays user-related information and derives three render states from loginStatus + currentUser.isVip: - guest (notLoggedIn): welcome panel + Sign in / Sign up entries - member (logged in, no VIP): user info card + Get VIP CTA + Log out - vip (logged in, VIP): user info card with VIP badge + Manage VIP CTA + Log out Adds UserView.isVip (default false), patches user-machine.toView() to populate it, and introduces three presentational components (GuestPanel, UserInfoCard, VipCta) co-located under src/app/sidebar. Preserves existing UserInit + logout-redirect behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { ROUTES } from "@/router/routes";
|
||||||
|
|
||||||
|
import styles from "./sidebar-screen.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未登录态欢迎面板
|
||||||
|
*
|
||||||
|
* 显示欢迎语与 Sign in / Sign up 入口。
|
||||||
|
*/
|
||||||
|
export function GuestPanel() {
|
||||||
|
return (
|
||||||
|
<div className={styles.guestPanel}>
|
||||||
|
<h2 className={styles.guestTitle}>Welcome to CozSweet</h2>
|
||||||
|
<p className={styles.guestSubtitle}>
|
||||||
|
Sign in to sync your chats and unlock VIP.
|
||||||
|
</p>
|
||||||
|
<div className={styles.guestActions}>
|
||||||
|
<Link
|
||||||
|
href={ROUTES.auth}
|
||||||
|
className={styles.guestPrimaryLink}
|
||||||
|
aria-label="Sign up"
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={ROUTES.auth}
|
||||||
|
className={styles.guestSecondaryLink}
|
||||||
|
aria-label="Sign in"
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,4 +2,7 @@
|
|||||||
* @file Automatically generated by barrelsby.
|
* @file Automatically generated by barrelsby.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./guest-panel";
|
||||||
export * from "./sidebar-screen";
|
export * from "./sidebar-screen";
|
||||||
|
export * from "./user-info-card";
|
||||||
|
export * from "./vip-cta";
|
||||||
|
|||||||
@@ -47,3 +47,229 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-lg, 16px);
|
gap: var(--spacing-lg, 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 用户信息区
|
||||||
|
* ============================================================ */
|
||||||
|
.profileArea {
|
||||||
|
padding: 0 var(--spacing-md, 12px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-lg, 16px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoCard {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md, 12px);
|
||||||
|
padding: var(--spacing-lg, 16px);
|
||||||
|
background: var(--color-settings-card-background, #ffffff);
|
||||||
|
border-radius: var(--radius-lg, 12px);
|
||||||
|
color: #000;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoAvatar {
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoAvatarFallback {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
background: var(--color-settings-row-background, #f2f2f2);
|
||||||
|
color: #000;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-size-xl, 20px);
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoText {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs, 4px);
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoNameRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm, 8px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoName {
|
||||||
|
font-size: var(--font-size-lg, 16px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoEmail {
|
||||||
|
font-size: var(--font-size-sm, 14px);
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfoCoinRow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs, 4px);
|
||||||
|
font-size: var(--font-size-sm, 14px);
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vipBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px var(--spacing-sm, 8px);
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
background: var(--color-accent, #7c5cff);
|
||||||
|
color: var(--color-text-primary, #ffffff);
|
||||||
|
font-size: var(--font-size-xs, 12px);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vipCta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-md, 12px) var(--spacing-lg, 16px);
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-button-gradient-start, #7c5cff),
|
||||||
|
var(--color-button-gradient-end, #b48bff)
|
||||||
|
);
|
||||||
|
color: var(--color-text-primary, #ffffff);
|
||||||
|
font-size: var(--font-size-lg, 16px);
|
||||||
|
font-weight: 600;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vipCta:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vipCta:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent, #7c5cff);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 未登录态 GuestPanel
|
||||||
|
* ============================================================ */
|
||||||
|
.guestPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md, 12px);
|
||||||
|
padding: var(--spacing-lg, 16px);
|
||||||
|
background: var(--color-settings-card-background, #ffffff);
|
||||||
|
border-radius: var(--radius-lg, 12px);
|
||||||
|
color: #000;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestTitle {
|
||||||
|
font-size: var(--font-size-xl, 20px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestSubtitle {
|
||||||
|
font-size: var(--font-size-sm, 14px);
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestActions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm, 8px);
|
||||||
|
margin-top: var(--spacing-sm, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestPrimaryLink {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-md, 12px) var(--spacing-lg, 16px);
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-button-gradient-start, #7c5cff),
|
||||||
|
var(--color-button-gradient-end, #b48bff)
|
||||||
|
);
|
||||||
|
color: var(--color-text-primary, #ffffff);
|
||||||
|
font-size: var(--font-size-lg, 16px);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestPrimaryLink:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestSecondaryLink {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-md, 12px) var(--spacing-lg, 16px);
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
background: transparent;
|
||||||
|
color: #000;
|
||||||
|
font-size: var(--font-size-lg, 16px);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid var(--color-auth-border, #d0d0d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestSecondaryLink:hover {
|
||||||
|
background: var(--color-settings-row-background, #f2f2f2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 骨架占位
|
||||||
|
* ============================================================ */
|
||||||
|
.skeleton {
|
||||||
|
display: block;
|
||||||
|
background: var(--color-settings-row-background, #e5e5e5);
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonMd {
|
||||||
|
width: 60%;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonSm {
|
||||||
|
width: 80%;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonXs {
|
||||||
|
width: 40%;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,21 +7,33 @@ import { useRouter } from "next/navigation";
|
|||||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||||||
import { SettingsSection } from "@/app/_components/core/settings-section";
|
import { SettingsSection } from "@/app/_components/core/settings-section";
|
||||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||||
import { useAuthDispatch } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
|
|
||||||
import type { UserView } from "@/models/user/user";
|
import type { UserView } from "@/models/user/user";
|
||||||
|
|
||||||
|
import { GuestPanel } from "./guest-panel";
|
||||||
|
import { UserInfoCard } from "./user-info-card";
|
||||||
|
import { VipCta } from "./vip-cta";
|
||||||
|
|
||||||
import styles from "./sidebar-screen.module.css";
|
import styles from "./sidebar-screen.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 侧边栏主屏幕
|
* 侧边栏主屏幕
|
||||||
*
|
*
|
||||||
|
* 根据 `loginStatus + currentUser.isVip` 派生三种状态:
|
||||||
|
* - guest:未登录,展示欢迎面板
|
||||||
|
* - member:已登录未购买 VIP,展示用户信息 + Get VIP CTA
|
||||||
|
* - vip:已登录且购买 VIP,展示用户信息(含 VIP 徽章) + Manage VIP CTA
|
||||||
|
*
|
||||||
* 原始 Dart: lib/ui/sidebar/sidebar_screen.dart + profile_view.dart
|
* 原始 Dart: lib/ui/sidebar/sidebar_screen.dart + profile_view.dart
|
||||||
*/
|
*/
|
||||||
|
type SidebarState = "guest" | "member" | "vip";
|
||||||
|
|
||||||
export function SidebarScreen() {
|
export function SidebarScreen() {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const userDispatch = useUserDispatch();
|
const userDispatch = useUserDispatch();
|
||||||
|
const auth = useAuthState();
|
||||||
const authDispatch = useAuthDispatch();
|
const authDispatch = useAuthDispatch();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -45,6 +57,14 @@ export function SidebarScreen() {
|
|||||||
}
|
}
|
||||||
}, [user.currentUser, authDispatch, router]);
|
}, [user.currentUser, authDispatch, router]);
|
||||||
|
|
||||||
|
// 状态派生:未登录 / 已登录未 VIP / 已登录 VIP
|
||||||
|
const sidebarState: SidebarState =
|
||||||
|
auth.loginStatus === "notLoggedIn"
|
||||||
|
? "guest"
|
||||||
|
: user.currentUser?.isVip === true
|
||||||
|
? "vip"
|
||||||
|
: "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell>
|
<MobileShell>
|
||||||
<div className={styles.shell}>
|
<div className={styles.shell}>
|
||||||
@@ -71,7 +91,34 @@ export function SidebarScreen() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 设置区域:与 Flutter SettingsSection 对齐 */}
|
{/* 用户信息区:三种状态 */}
|
||||||
|
<div className={styles.profileArea}>
|
||||||
|
{sidebarState === "guest" ? (
|
||||||
|
<GuestPanel />
|
||||||
|
) : user.currentUser == null ? (
|
||||||
|
// 已登录但 currentUser 仍在初始化 → 骨架占位
|
||||||
|
<div className={styles.userInfoCard} aria-hidden="true">
|
||||||
|
<div className={styles.userInfoAvatarFallback} />
|
||||||
|
<div className={styles.userInfoText}>
|
||||||
|
<div className={`${styles.skeleton} ${styles.skeletonMd}`} />
|
||||||
|
<div className={`${styles.skeleton} ${styles.skeletonSm}`} />
|
||||||
|
<div className={`${styles.skeleton} ${styles.skeletonXs}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UserInfoCard
|
||||||
|
user={user.currentUser}
|
||||||
|
coinBalance={user.coinBalance}
|
||||||
|
isVip={sidebarState === "vip"}
|
||||||
|
/>
|
||||||
|
<VipCta variant={sidebarState === "vip" ? "manage" : "get"} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 设置区域:仅已登录态保留 Log out;与 Flutter SettingsSection 对齐 */}
|
||||||
|
{sidebarState !== "guest" ? (
|
||||||
<div className={styles.settingsArea}>
|
<div className={styles.settingsArea}>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title="Other"
|
title="Other"
|
||||||
@@ -90,6 +137,7 @@ export function SidebarScreen() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import type { UserView } from "@/models/user/user";
|
||||||
|
|
||||||
|
import styles from "./sidebar-screen.module.css";
|
||||||
|
|
||||||
|
export interface UserInfoCardProps {
|
||||||
|
user: UserView;
|
||||||
|
coinBalance: number;
|
||||||
|
isVip: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已登录用户信息卡片
|
||||||
|
*
|
||||||
|
* 头像 + 用户名(+ VIP 徽章)+ 邮箱 + 金币余额。
|
||||||
|
*/
|
||||||
|
export function UserInfoCard({ user, coinBalance, isVip }: UserInfoCardProps) {
|
||||||
|
const initial = (user.username?.[0] ?? "?").toUpperCase();
|
||||||
|
const hasAvatar = user.avatarUrl && user.avatarUrl.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.userInfoCard}>
|
||||||
|
{hasAvatar ? (
|
||||||
|
<Image
|
||||||
|
src={user.avatarUrl}
|
||||||
|
alt=""
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className={styles.userInfoAvatar}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles.userInfoAvatarFallback}
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
aria-label={user.username}
|
||||||
|
>
|
||||||
|
{initial}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.userInfoText}>
|
||||||
|
<div className={styles.userInfoNameRow}>
|
||||||
|
<span className={styles.userInfoName} title={user.username}>
|
||||||
|
{user.username}
|
||||||
|
</span>
|
||||||
|
{isVip ? (
|
||||||
|
<span className={styles.vipBadge} aria-label="VIP">
|
||||||
|
VIP
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{user.email ? (
|
||||||
|
<span className={styles.userInfoEmail} title={user.email}>
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.userInfoCoinRow}>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="9"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{coinBalance} coins</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { ROUTES } from "@/router/routes";
|
||||||
|
|
||||||
|
import styles from "./sidebar-screen.module.css";
|
||||||
|
|
||||||
|
export type VipCtaVariant = "get" | "manage";
|
||||||
|
|
||||||
|
export interface VipCtaProps {
|
||||||
|
variant: VipCtaVariant;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VIP CTA 按钮
|
||||||
|
*
|
||||||
|
* - `get`:未购买 VIP 时显示 "Get VIP"
|
||||||
|
* - `manage`:已购买 VIP 时显示 "Manage VIP"
|
||||||
|
*
|
||||||
|
* 默认点击跳转 `/subscription`(订阅页);调用方可传入 `onClick` 覆盖。
|
||||||
|
*/
|
||||||
|
export function VipCta({ variant, onClick }: VipCtaProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const label = variant === "get" ? "Get VIP" : "Manage VIP";
|
||||||
|
const handleClick = () => {
|
||||||
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
} else {
|
||||||
|
router.push(ROUTES.subscription);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.vipCta}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import { GuestChatQuota as GuestChatQuotaDto } from "@/data/dto/chat/guest_chat_
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { type Result as ResultT, Result } from "@/utils/result";
|
import { type Result as ResultT, Result } from "@/utils/result";
|
||||||
import { formatDate } from "@/utils/date";
|
import { todayString } from "@/utils/date";
|
||||||
import { SpAsyncUtil } from "@/utils/storage";
|
import { SpAsyncUtil } from "@/utils/storage";
|
||||||
import { StorageKeys } from "../storage_keys";
|
import { StorageKeys } from "../storage_keys";
|
||||||
import type { IChatStorage } from "./ichat_storage";
|
import type { IChatStorage } from "./ichat_storage";
|
||||||
@@ -51,7 +51,7 @@ export class ChatStorage implements IChatStorage {
|
|||||||
if (existing.success && existing.data != null) {
|
if (existing.success && existing.data != null) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
const today = formatDate(new Date());
|
const today = todayString(new Date());
|
||||||
const initResult = await this.setGuestDailyChatQuota(
|
const initResult = await this.setGuestDailyChatQuota(
|
||||||
GuestChatQuotaDto.threshold,
|
GuestChatQuotaDto.threshold,
|
||||||
today,
|
today,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const UserViewSchema = z.object({
|
|||||||
relationshipStage: z.string(),
|
relationshipStage: z.string(),
|
||||||
currentMood: z.string(),
|
currentMood: z.string(),
|
||||||
isGuest: z.boolean(),
|
isGuest: z.boolean(),
|
||||||
|
isVip: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UserView = z.infer<typeof UserViewSchema>;
|
export type UserView = z.infer<typeof UserViewSchema>;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ function toView(u: {
|
|||||||
relationshipStage?: string;
|
relationshipStage?: string;
|
||||||
currentMood?: string;
|
currentMood?: string;
|
||||||
isGuest?: boolean;
|
isGuest?: boolean;
|
||||||
|
isVip?: boolean;
|
||||||
}): UserView {
|
}): UserView {
|
||||||
return {
|
return {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
@@ -60,6 +61,7 @@ function toView(u: {
|
|||||||
relationshipStage: u.relationshipStage ?? "",
|
relationshipStage: u.relationshipStage ?? "",
|
||||||
currentMood: u.currentMood ?? "",
|
currentMood: u.currentMood ?? "",
|
||||||
isGuest: u.isGuest ?? false,
|
isGuest: u.isGuest ?? false,
|
||||||
|
isVip: u.isVip ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user