Files
cozsweet-frontend-nextjs/src/app/chat/components/chat-insufficient-credits-banner.tsx
T

93 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* ChatInsufficientCreditsBanner 积分不足横幅
*
* 何时显示:
* - 后端返回 canSendMessage=false
*
* 视觉规格(与设计稿对齐):
* - 粉→品红渐变背景(与 splash 渐变一致)
* - 大字号粉色/白色提示文案
* - 粉→品红渐变 pill 按钮 → 跳 /subscription?type=topup
*
* 业务关系:
* - 与订阅页 top up 入口行为一致
*
* 不做的事:
* - 不直接管理 state machinechat 机器不感知 UI 层)
*/
import { useAppNavigator } from "@/router/use-app-navigator";
import type { PaymentGuidance } from "@/data/schemas/chat";
export interface ChatInsufficientCreditsBannerProps {
title?: string;
description?: string;
ctaLabel?: string | null;
guidance?: PaymentGuidance | null;
/**
* 自定义点击回调(不传则默认走统一订阅导航)
* - 测试时可传 mock
* - 嵌入其他场景时(如 nested overlay)可传自定义
*/
onUnlock?: () => void;
}
export function ChatInsufficientCreditsBanner({
title = "Insufficient credits\nTop up to continue chatting",
description,
ctaLabel = "Top up credits to continue",
guidance = null,
onUnlock,
}: ChatInsufficientCreditsBannerProps) {
const navigator = useAppNavigator();
const activeGuidance = guidance;
const titleLines = title.split("\n");
const handleClick = () => {
if (onUnlock) {
onUnlock();
return;
}
navigator.openSubscription({ type: "topup", returnTo: "chat" });
};
return (
<div
className="mx-(--chat-inline-padding,16px) mb-(--page-section-gap,16px) shrink-0 rounded-(--responsive-card-radius-sm,24px) bg-[linear-gradient(135deg,#f96ade_0%,#f657a0_100%)] px-(--responsive-card-padding,16px) py-(--page-section-gap-lg,24px) text-center shadow-[0_4px_16px_rgba(248,77,150,0.25)]"
role="status"
aria-live="polite"
data-testid="chat-insufficient-credits-banner"
data-payment-guidance-id={activeGuidance?.guidanceId}
>
<p className="m-0 mb-(--spacing-sm,8px) text-(length:--responsive-card-title,18px) font-semibold leading-[1.4] text-white opacity-95">
{titleLines.map((line, index) => (
<span key={`${line}-${index}`}>
{line}
{index < titleLines.length - 1 ? <br /> : null}
</span>
))}
</p>
{description ? (
<p className="mx-auto mb-(--spacing-lg,16px) mt-0 max-w-[min(100%,360px)] text-(length:--responsive-caption,14px) font-semibold leading-[1.4] text-[rgba(255,255,255,0.88)]">
{description}
</p>
) : null}
{activeGuidance?.copy ? (
<p className="mx-auto mb-(--spacing-lg,16px) mt-0 max-w-[min(100%,360px)] text-(length:--responsive-caption,14px) font-semibold leading-[1.4] text-white">
{activeGuidance.copy}
</p>
) : null}
{ctaLabel ? <button
type="button"
data-analytics-key="chat.topup"
data-analytics-label="Top up chat credits"
className="min-h-(--responsive-control-height,48px) w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(135deg,#ffb8e0_0%,#f57ec0_100%)] px-(--responsive-card-padding-lg,24px) text-(length:--responsive-card-title,18px) font-bold text-white shadow-[0_2px_8px_rgba(0,0,0,0.15)] transition-[box-shadow,transform] duration-150 hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] active:scale-98"
onClick={handleClick}
aria-label={ctaLabel}
>
{ctaLabel}
</button> : null}
</div>
);
}