mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix: subscription UI improvements - expired card, duplicate badges, live countdown
- SubscriptionCardExpired: use trialTitle for expired trials, remove traffic/devices grid, show only expiry date - Subscription page: remove duplicate trial badge from zone indicator, fix expired trial badge showing "Пробный период" instead of "Истекла" - Subscription page: replace static days counter with live HH:MM:SS countdown timer, merge "Действует до" into countdown block - Subscription page: remove duplicate traffic percentage block - Handle expired state in countdown (show "Истекла" in red instead of 00:00:00) - Use live countdown.days for urgency threshold instead of stale server value
This commit is contained in:
@@ -77,7 +77,9 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
||||
{t('dashboard.expired.title')}
|
||||
{subscription.is_trial
|
||||
? t('dashboard.expired.trialTitle')
|
||||
: t('dashboard.expired.title')}
|
||||
</h2>
|
||||
<span className="text-xs text-dark-50/35">
|
||||
{subscription.is_trial
|
||||
@@ -117,27 +119,21 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expired info grid */}
|
||||
{/* Expired date */}
|
||||
<div
|
||||
className="mb-5 flex justify-around rounded-[14px] text-center"
|
||||
className="mb-5 flex items-center justify-center rounded-[14px]"
|
||||
style={{
|
||||
background: 'rgba(255,59,92,0.04)',
|
||||
border: '1px solid rgba(255,59,92,0.08)',
|
||||
padding: '16px 18px',
|
||||
padding: '14px 18px',
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: t('dashboard.expired.traffic'), value: '0 GB' },
|
||||
{ label: t('dashboard.expired.devices'), value: '0' },
|
||||
{ label: t('dashboard.expired.expiredDate'), value: formattedDate },
|
||||
].map((item, i) => (
|
||||
<div key={i}>
|
||||
<div className="mb-1 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="text-base font-bold tracking-tight text-dark-50/50">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mb-0.5 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
|
||||
{t('dashboard.expired.expiredDate')}
|
||||
</div>
|
||||
<div className="ml-3 text-base font-bold tracking-tight text-dark-50/50">
|
||||
{formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
|
||||
@@ -187,6 +187,25 @@ export default function Subscription() {
|
||||
// Extract subscription from response (null if no subscription)
|
||||
const subscription = subscriptionResponse?.subscription ?? null;
|
||||
|
||||
// Live countdown timer
|
||||
const [countdown, setCountdown] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
useEffect(() => {
|
||||
if (!subscription?.end_date) return;
|
||||
const endTime = new Date(subscription.end_date).getTime();
|
||||
const tick = () => {
|
||||
const diff = Math.max(0, endTime - Date.now());
|
||||
setCountdown({
|
||||
days: Math.floor(diff / 86_400_000),
|
||||
hours: Math.floor((diff % 86_400_000) / 3_600_000),
|
||||
minutes: Math.floor((diff % 3_600_000) / 60_000),
|
||||
seconds: Math.floor((diff % 60_000) / 1_000),
|
||||
});
|
||||
};
|
||||
tick();
|
||||
const id = setInterval(tick, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [subscription?.end_date]);
|
||||
|
||||
const { data: purchaseOptions, isLoading: optionsLoading } = useQuery({
|
||||
queryKey: ['purchase-options'],
|
||||
queryFn: subscriptionApi.getPurchaseOptions,
|
||||
@@ -679,7 +698,6 @@ export default function Subscription() {
|
||||
const zone = getTrafficZone(usedPercent);
|
||||
const connectedDevices = devicesData?.total ?? 0;
|
||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
||||
const daysLeft = subscription.days_left;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -736,19 +754,6 @@ export default function Subscription() {
|
||||
>
|
||||
{isUnlimited ? t('dashboard.unlimited') : t(zone.labelKey)}
|
||||
</span>
|
||||
{subscription.is_trial && (
|
||||
<span
|
||||
className="inline-flex animate-trial-glow items-center gap-1 rounded-md px-2 py-0.5 font-mono text-[9px] font-bold uppercase tracking-widest"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(62,219,176,0.15), rgba(62,219,176,0.06))',
|
||||
border: '1px solid rgba(62,219,176,0.2)',
|
||||
color: '#3EDBB0',
|
||||
}}
|
||||
>
|
||||
{t('subscription.trialStatus')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Plan name */}
|
||||
@@ -770,11 +775,11 @@ export default function Subscription() {
|
||||
color: subscription.is_active ? zone.mainHex : '#FF3B5C',
|
||||
}}
|
||||
>
|
||||
{subscription.is_trial
|
||||
? t('subscription.trialStatus')
|
||||
: subscription.is_active
|
||||
? t('subscription.active')
|
||||
: t('subscription.expired')}
|
||||
{subscription.is_active
|
||||
? subscription.is_trial
|
||||
? t('subscription.trialStatus')
|
||||
: t('subscription.active')
|
||||
: t('subscription.expired')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -992,57 +997,114 @@ export default function Subscription() {
|
||||
|
||||
{/* ─── Stats Row ─── */}
|
||||
<div className="mb-5 grid grid-cols-2 gap-2.5">
|
||||
{/* Days left */}
|
||||
<div
|
||||
className="rounded-[14px] p-3.5"
|
||||
style={{
|
||||
background: daysLeft <= 3 ? 'rgba(255,184,0,0.06)' : g.innerBg,
|
||||
border:
|
||||
daysLeft <= 3
|
||||
? '1px solid rgba(255,184,0,0.15)'
|
||||
: `1px solid ${g.innerBorder}`,
|
||||
}}
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||
{/* Countdown timer */}
|
||||
{(() => {
|
||||
const isUrgent = countdown.days <= 3;
|
||||
const isExpired = !subscription.is_active;
|
||||
return (
|
||||
<div
|
||||
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
||||
className="rounded-[14px] p-3.5"
|
||||
style={{
|
||||
background: daysLeft <= 3 ? 'rgba(255,184,0,0.1)' : g.hoverBg,
|
||||
background: isExpired
|
||||
? 'rgba(255,59,92,0.06)'
|
||||
: isUrgent
|
||||
? 'rgba(255,184,0,0.06)'
|
||||
: g.innerBg,
|
||||
border: isExpired
|
||||
? '1px solid rgba(255,59,92,0.15)'
|
||||
: isUrgent
|
||||
? '1px solid rgba(255,184,0,0.15)'
|
||||
: `1px solid ${g.innerBorder}`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={daysLeft <= 3 ? '#FFB800' : g.textSecondary}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||
<div
|
||||
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
||||
style={{
|
||||
background: isExpired
|
||||
? 'rgba(255,59,92,0.1)'
|
||||
: isUrgent
|
||||
? 'rgba(255,184,0,0.1)'
|
||||
: g.hoverBg,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={isExpired ? '#FF3B5C' : isUrgent ? '#FFB800' : g.textSecondary}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
</div>
|
||||
{t('dashboard.remaining')}
|
||||
</div>
|
||||
{isExpired ? (
|
||||
<div
|
||||
className="text-[18px] font-bold tracking-tight"
|
||||
style={{ color: '#FF3B5C' }}
|
||||
>
|
||||
{t('subscription.expired')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-baseline gap-1 font-mono tabular-nums">
|
||||
{countdown.days > 0 && (
|
||||
<>
|
||||
<span
|
||||
className="text-[20px] font-bold tracking-tight"
|
||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||
>
|
||||
{countdown.days}
|
||||
</span>
|
||||
<span className="mr-1 text-[10px] font-medium text-dark-50/25">
|
||||
{t('subscription.daysShort')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
className="text-[20px] font-bold tracking-tight"
|
||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||
>
|
||||
{String(countdown.hours).padStart(2, '0')}
|
||||
</span>
|
||||
<span
|
||||
className="mx-[-1px] text-[16px] font-bold opacity-30"
|
||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
className="text-[20px] font-bold tracking-tight"
|
||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||
>
|
||||
{String(countdown.minutes).padStart(2, '0')}
|
||||
</span>
|
||||
<span
|
||||
className="mx-[-1px] text-[16px] font-bold opacity-30"
|
||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
className="text-[20px] font-bold tracking-tight"
|
||||
style={{ color: isUrgent ? '#FFB800' : g.text }}
|
||||
>
|
||||
{String(countdown.seconds).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 text-[10px] font-medium text-dark-50/25">
|
||||
{t('subscription.expiresAt')}: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
{t('dashboard.remaining')}
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
className="text-[22px] font-bold tracking-tight"
|
||||
style={{ color: daysLeft <= 3 ? '#FFB800' : g.text }}
|
||||
>
|
||||
{daysLeft > 0
|
||||
? daysLeft
|
||||
: subscription.hours_left > 0
|
||||
? `${subscription.hours_left}h`
|
||||
: `${subscription.minutes_left}m`}
|
||||
</span>
|
||||
<span className="text-xs font-medium text-dark-50/25">
|
||||
{daysLeft > 0 ? t('subscription.daysShort') : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Devices */}
|
||||
<div
|
||||
@@ -1052,7 +1114,7 @@ export default function Subscription() {
|
||||
border: `1px solid ${g.innerBorder}`,
|
||||
}}
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||
<div
|
||||
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
||||
style={{ background: `${zone.mainHex}12` }}
|
||||
@@ -1083,41 +1145,6 @@ export default function Subscription() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expires */}
|
||||
<div
|
||||
className="rounded-[14px] p-3.5"
|
||||
style={{
|
||||
background: g.innerBg,
|
||||
border: `1px solid ${g.innerBorder}`,
|
||||
}}
|
||||
>
|
||||
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||
{t('subscription.expiresAt')}
|
||||
</div>
|
||||
<div className="text-sm font-semibold tracking-tight text-dark-50">
|
||||
{formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Traffic */}
|
||||
<div
|
||||
className="rounded-[14px] p-3.5"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${zone.mainHex}08, ${zone.mainHex}03)`,
|
||||
border: `1px solid ${zone.mainHex}12`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mb-1 text-[10px] font-semibold uppercase tracking-wider opacity-70"
|
||||
style={{ color: zone.mainHex }}
|
||||
>
|
||||
{t('subscription.traffic')}
|
||||
</div>
|
||||
<div className="text-sm font-semibold tracking-tight text-dark-50">
|
||||
{isUnlimited ? '∞' : `${usedPercent.toFixed(0)}%`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Locations ─── */}
|
||||
|
||||
Reference in New Issue
Block a user