mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
feat: add LIMITED subscription status support with traffic-exhausted UX
- Add is_limited to Subscription type - Show yellow traffic-exhausted badge on Subscription and AdminUsers pages - SubscriptionCardExpired: amber theme with warning icon and buy-traffic CTA for limited - Dashboard: route limited subscriptions to SubscriptionCardExpired - Subscription page: show additional options (buy traffic) for limited subs - Use is_limited boolean instead of status string comparison - Add trafficLimited translations for ru, en, zh, fa - AdminUserDetail StatusBadge: add limited yellow styling
This commit is contained in:
@@ -38,6 +38,9 @@ export default function SubscriptionCardExpired({
|
||||
|
||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
||||
|
||||
// Detect limited (traffic exhausted) state
|
||||
const isLimited = subscription.is_limited;
|
||||
|
||||
// Detect daily subscription (disabled or expired)
|
||||
const isDaily = subscription.is_daily;
|
||||
const isDisabledDaily = subscription.status === 'disabled' && isDaily;
|
||||
@@ -92,19 +95,38 @@ export default function SubscriptionCardExpired({
|
||||
navigate(`/balance/top-up?${params.toString()}`);
|
||||
};
|
||||
|
||||
// Color scheme: amber for limited, red for expired/disabled
|
||||
const accent = isLimited
|
||||
? {
|
||||
r: 255,
|
||||
g: 184,
|
||||
b: 0,
|
||||
hex: '#FFB800',
|
||||
gradient: 'linear-gradient(135deg, #FFB800, #FF8C00)',
|
||||
}
|
||||
: {
|
||||
r: 255,
|
||||
g: 59,
|
||||
b: 92,
|
||||
hex: '#FF3B5C',
|
||||
gradient: 'linear-gradient(135deg, #FF3B5C, #FF6B35)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-3xl ${className ?? ''}`}
|
||||
style={{
|
||||
background: g.cardBg,
|
||||
border: isDark ? '1px solid rgba(255,70,70,0.12)' : '1px solid rgba(255,59,92,0.2)',
|
||||
border: isDark
|
||||
? `1px solid rgba(${accent.r},${accent.g},${accent.b},0.12)`
|
||||
: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
boxShadow: isDark
|
||||
? g.shadow
|
||||
: '0 2px 16px rgba(255,59,92,0.1), 0 0 0 1px rgba(255,59,92,0.06)',
|
||||
: `0 2px 16px rgba(${accent.r},${accent.g},${accent.b},0.1), 0 0 0 1px rgba(${accent.r},${accent.g},${accent.b},0.06)`,
|
||||
padding: '28px 28px 24px',
|
||||
}}
|
||||
>
|
||||
{/* Red glow */}
|
||||
{/* Glow */}
|
||||
<div
|
||||
className="pointer-events-none absolute"
|
||||
style={{
|
||||
@@ -113,7 +135,7 @@ export default function SubscriptionCardExpired({
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(255,59,92,0.08) 0%, transparent 70%)',
|
||||
background: `radial-gradient(circle, rgba(${accent.r},${accent.g},${accent.b},0.08) 0%, transparent 70%)`,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -134,44 +156,70 @@ export default function SubscriptionCardExpired({
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
{/* Clock icon */}
|
||||
<div
|
||||
className="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[14px]"
|
||||
style={{
|
||||
background: 'rgba(255,59,92,0.1)',
|
||||
border: '1px solid rgba(255,59,92,0.15)',
|
||||
background: `rgba(${accent.r},${accent.g},${accent.b},0.1)`,
|
||||
border: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.15)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#FF3B5C"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
{isLimited ? (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
||||
{isDisabledDaily
|
||||
? t('dashboard.suspended.title')
|
||||
: subscription.is_trial
|
||||
? t('dashboard.expired.trialTitle')
|
||||
: t('dashboard.expired.title')}
|
||||
{isLimited
|
||||
? t('subscription.trafficLimitedTitle')
|
||||
: isDisabledDaily
|
||||
? t('dashboard.suspended.title')
|
||||
: subscription.is_trial
|
||||
? t('dashboard.expired.trialTitle')
|
||||
: t('dashboard.expired.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Limited description */}
|
||||
{isLimited && (
|
||||
<p className="mb-4 text-sm text-dark-50/60">
|
||||
{t('subscription.trafficLimitedDescription')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Expired date + Balance row */}
|
||||
<div
|
||||
className="mb-5 flex items-center justify-between rounded-[14px]"
|
||||
style={{
|
||||
background: 'rgba(255,59,92,0.04)',
|
||||
border: '1px solid rgba(255,59,92,0.08)',
|
||||
background: `rgba(${accent.r},${accent.g},${accent.b},0.04)`,
|
||||
border: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.08)`,
|
||||
padding: '14px 18px',
|
||||
}}
|
||||
>
|
||||
@@ -207,95 +255,123 @@ export default function SubscriptionCardExpired({
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2.5">
|
||||
{/* Quick Renew or Top Up button (hidden for expired trials) */}
|
||||
{!subscription.is_trial && (
|
||||
{isLimited ? (
|
||||
<Link
|
||||
to="/subscription"
|
||||
className="flex flex-1 items-center justify-center gap-2 rounded-[14px] py-3.5 text-[15px] font-semibold tracking-tight text-white transition-all duration-300"
|
||||
style={{
|
||||
background: accent.gradient,
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
{t('subscription.trafficLimited')}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
{hasBalance ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleQuickRenew}
|
||||
disabled={isRenewing}
|
||||
className="flex flex-1 items-center justify-center gap-2 rounded-[14px] py-3.5 text-[15px] font-semibold tracking-tight text-white transition-all duration-300 disabled:opacity-50"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #FF3B5C, #FF6B35)',
|
||||
boxShadow: '0 4px 20px rgba(255,59,92,0.2)',
|
||||
}}
|
||||
>
|
||||
{isRenewing ? (
|
||||
<span
|
||||
className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{/* Quick Renew or Top Up button (hidden for expired trials) */}
|
||||
{!subscription.is_trial && (
|
||||
<>
|
||||
{hasBalance ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleQuickRenew}
|
||||
disabled={isRenewing}
|
||||
className="flex flex-1 items-center justify-center gap-2 rounded-[14px] py-3.5 text-[15px] font-semibold tracking-tight text-white transition-all duration-300 disabled:opacity-50"
|
||||
style={{
|
||||
background: accent.gradient,
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
{isRenewing ? (
|
||||
<span
|
||||
className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
)}
|
||||
{isRenewing
|
||||
? t('common.loading')
|
||||
: isDisabledDaily
|
||||
? t('dashboard.suspended.resume')
|
||||
: t('dashboard.expired.quickRenew')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTopUp}
|
||||
className="flex flex-1 items-center justify-center gap-2 rounded-[14px] py-3.5 text-[15px] font-semibold tracking-tight text-white transition-all duration-300"
|
||||
style={{
|
||||
background: accent.gradient,
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
{t('dashboard.expired.topUp')}
|
||||
</button>
|
||||
)}
|
||||
{isRenewing
|
||||
? t('common.loading')
|
||||
: isDisabledDaily
|
||||
? t('dashboard.suspended.resume')
|
||||
: t('dashboard.expired.quickRenew')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTopUp}
|
||||
className="flex flex-1 items-center justify-center gap-2 rounded-[14px] py-3.5 text-[15px] font-semibold tracking-tight text-white transition-all duration-300"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #FF3B5C, #FF6B35)',
|
||||
boxShadow: '0 4px 20px rgba(255,59,92,0.2)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
{t('dashboard.expired.topUp')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tariffs (go to purchase page) — full-width for trials */}
|
||||
<Link
|
||||
to="/subscription/purchase"
|
||||
className={`flex items-center justify-center rounded-[14px] px-5 py-3.5 text-[15px] font-semibold tracking-tight transition-colors duration-200 ${
|
||||
subscription.is_trial ? 'flex-1 text-white' : 'text-dark-50/50'
|
||||
}`}
|
||||
style={
|
||||
subscription.is_trial
|
||||
? {
|
||||
background: accent.gradient,
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}
|
||||
: {
|
||||
background: g.innerBg,
|
||||
border: `1px solid ${g.innerBorder}`,
|
||||
}
|
||||
}
|
||||
>
|
||||
{t('dashboard.expired.tariffs')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tariffs (go to purchase page) — full-width for trials */}
|
||||
<Link
|
||||
to="/subscription/purchase"
|
||||
className={`flex items-center justify-center rounded-[14px] px-5 py-3.5 text-[15px] font-semibold tracking-tight transition-colors duration-200 ${
|
||||
subscription.is_trial ? 'flex-1 text-white' : 'text-dark-50/50'
|
||||
}`}
|
||||
style={
|
||||
subscription.is_trial
|
||||
? {
|
||||
background: 'linear-gradient(135deg, #FF3B5C, #FF6B35)',
|
||||
boxShadow: '0 4px 20px rgba(255,59,92,0.2)',
|
||||
}
|
||||
: {
|
||||
background: g.innerBg,
|
||||
border: `1px solid ${g.innerBorder}`,
|
||||
}
|
||||
}
|
||||
>
|
||||
{t('dashboard.expired.tariffs')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user