Add subscription expiry reminders across account and dashboard
This commit is contained in:
@@ -2654,6 +2654,35 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.root :global(.account-renew-badge) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.34);
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #fbbf24;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.root :global(.account-renew-badge:hover) {
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
border-color: rgba(245, 158, 11, 0.6);
|
||||
color: #fff6d5;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.root :global(.account-renew-badge.expired) {
|
||||
border-color: rgba(239, 68, 68, 0.34);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.root :global(.info-btn) {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
|
||||
@@ -12,6 +12,20 @@ import {
|
||||
hasSupabasePublicEnv,
|
||||
} from "@/lib/supabase/client";
|
||||
|
||||
function parseExpiryInfo(raw?: string | null) {
|
||||
const text = String(raw || "").trim();
|
||||
if (!text) return null;
|
||||
const dt = new Date(text);
|
||||
if (Number.isNaN(dt.getTime())) return null;
|
||||
const diffMs = dt.getTime() - Date.now();
|
||||
const daysLeft = Math.ceil(diffMs / 86_400_000);
|
||||
return {
|
||||
date: dt,
|
||||
daysLeft,
|
||||
expired: diffMs <= 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function HeaderBar() {
|
||||
const store = useDashboardStore();
|
||||
const { locale, setLocale, t } = useI18n();
|
||||
@@ -56,6 +70,36 @@ export function HeaderBar() {
|
||||
const accountAria = isAuthenticated
|
||||
? t("header.accountAria")
|
||||
: t("header.signInAria");
|
||||
const expiryInfo = parseExpiryInfo(store.proAccess.subscriptionExpiresAt);
|
||||
const isTrialPlan = /trial/i.test(
|
||||
String(store.proAccess.subscriptionPlanCode || ""),
|
||||
);
|
||||
const showRenewReminder =
|
||||
isAuthenticated &&
|
||||
!store.proAccess.loading &&
|
||||
(
|
||||
(store.proAccess.subscriptionActive &&
|
||||
expiryInfo &&
|
||||
expiryInfo.daysLeft <= 3) ||
|
||||
(!store.proAccess.subscriptionActive && Boolean(expiryInfo))
|
||||
);
|
||||
const renewReminderLabel = !showRenewReminder
|
||||
? ""
|
||||
: !store.proAccess.subscriptionActive
|
||||
? isTrialPlan
|
||||
? locale === "en-US"
|
||||
? "Trial ended"
|
||||
: "试用已结束"
|
||||
: locale === "en-US"
|
||||
? "Pro expired"
|
||||
: "Pro 已到期"
|
||||
: isTrialPlan
|
||||
? locale === "en-US"
|
||||
? `Trial ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left`
|
||||
: `试用剩余 ${Math.max(expiryInfo?.daysLeft || 0, 0)} 天`
|
||||
: locale === "en-US"
|
||||
? `Pro ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left`
|
||||
: `Pro 还剩 ${Math.max(expiryInfo?.daysLeft || 0, 0)} 天`;
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
@@ -101,6 +145,20 @@ export function HeaderBar() {
|
||||
<span>{accountLabel}</span>
|
||||
</Link>
|
||||
|
||||
{showRenewReminder ? (
|
||||
<Link
|
||||
href="/account"
|
||||
className={clsx(
|
||||
"account-renew-badge",
|
||||
!store.proAccess.subscriptionActive && "expired",
|
||||
)}
|
||||
title={renewReminderLabel}
|
||||
aria-label={renewReminderLabel}
|
||||
>
|
||||
<span>{renewReminderLabel}</span>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
<div className="live-badge" id="liveBadge">
|
||||
<span className="pulse-dot" />
|
||||
<span>{t("header.live")}</span>
|
||||
|
||||
Reference in New Issue
Block a user