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:
Fringg
2026-03-12 18:33:37 +03:00
parent d9bfb8427c
commit b4f9f332cf
10 changed files with 281 additions and 126 deletions

View File

@@ -119,6 +119,7 @@ function StatusBadge({ status }: { status: string }) {
deleted: 'bg-dark-600 text-dark-400 border-dark-500',
trial: 'bg-accent-500/20 text-accent-400 border-accent-500/30',
expired: 'bg-warning-500/20 text-warning-400 border-warning-500/30',
limited: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
disabled: 'bg-dark-600 text-dark-400 border-dark-500',
};

View File

@@ -147,14 +147,18 @@ function UserRow({ user, onClick, formatAmount }: UserRowProps) {
? 'border-success-500/30 bg-success-500/20 text-success-400'
: user.subscription_status === 'trial'
? 'border-accent-500/30 bg-accent-500/20 text-accent-400'
: 'border-warning-500/30 bg-warning-500/20 text-warning-400'
: user.subscription_status === 'limited'
? 'border-yellow-500/30 bg-yellow-500/20 text-yellow-400'
: 'border-warning-500/30 bg-warning-500/20 text-warning-400'
}`}
>
{user.subscription_status === 'active'
? t('admin.users.status.subscription')
: user.subscription_status === 'trial'
? t('admin.users.status.trial')
: t('admin.users.status.expired')}
: user.subscription_status === 'limited'
? t('subscription.trafficLimited')
: t('admin.users.status.expired')}
</span>
)}
</div>

View File

@@ -247,7 +247,9 @@ export default function Dashboard() {
<div className="skeleton h-12 w-full rounded-xl" />
</div>
</div>
) : subscription?.is_expired || subscription?.status === 'disabled' ? (
) : subscription?.is_expired ||
subscription?.status === 'disabled' ||
subscription?.is_limited ? (
<SubscriptionCardExpired
subscription={subscription}
balanceKopeks={balanceData?.balance_kopeks ?? 0}

View File

@@ -518,23 +518,76 @@ export default function Subscription() {
style={{
background: subscription.is_active
? `${zone.mainHex}15`
: 'rgba(255,59,92,0.12)',
: subscription.is_limited
? 'rgba(255,184,0,0.12)'
: 'rgba(255,59,92,0.12)',
border: subscription.is_active
? `1px solid ${zone.mainHex}30`
: '1px solid rgba(255,59,92,0.25)',
color: subscription.is_active ? zone.mainHex : '#FF3B5C',
: subscription.is_limited
? '1px solid rgba(255,184,0,0.25)'
: '1px solid rgba(255,59,92,0.25)',
color: subscription.is_active
? zone.mainHex
: subscription.is_limited
? '#FFB800'
: '#FF3B5C',
}}
>
{subscription.is_active
? subscription.is_trial
? t('subscription.trialStatus')
: t('subscription.active')
: subscription.status === 'disabled'
? t('subscription.pause.suspended')
: t('subscription.expired')}
: subscription.is_limited
? t('subscription.trafficLimited')
: subscription.status === 'disabled'
? t('subscription.pause.suspended')
: t('subscription.expired')}
</span>
</div>
{/* ─── Traffic Limited Banner ─── */}
{subscription.is_limited && (
<div
className="mb-6 rounded-[14px] p-4"
style={{
background:
'linear-gradient(135deg, rgba(255,184,0,0.08), rgba(255,184,0,0.03))',
border: '1px solid rgba(255,184,0,0.2)',
}}
>
<div className="flex items-start gap-3">
<div
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px]"
style={{ background: 'rgba(255,184,0,0.12)' }}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#FFB800"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold" style={{ color: '#FFB800' }}>
{t('subscription.trafficLimitedTitle')}
</p>
<p className="mt-1 text-xs text-dark-400">
{t('subscription.trafficLimitedDescription')}
</p>
</div>
</div>
</div>
)}
{/* ─── Trial Info Banner ─── */}
{subscription.is_trial && subscription.is_active && (
<div
@@ -988,11 +1041,13 @@ export default function Subscription() {
{t('subscription.pause.title')}
</h2>
<div className="mt-1 text-[12px] text-dark-50/35">
{subscription.status === 'disabled'
? t('subscription.pause.suspended')
: subscription.is_daily_paused
? t('subscription.pause.paused')
: t('subscription.pause.active')}
{subscription.is_limited
? t('subscription.trafficLimited')
: subscription.status === 'disabled'
? t('subscription.pause.suspended')
: subscription.is_daily_paused
? t('subscription.pause.paused')
: t('subscription.pause.active')}
</div>
</div>
<button
@@ -1140,7 +1195,7 @@ export default function Subscription() {
{/* Additional Options (Buy Devices) */}
{subscription &&
subscription.is_active &&
(subscription.is_active || subscription.is_limited) &&
!subscription.is_trial &&
subscription.device_limit !== 0 && (
<div