mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13: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>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
<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>
|
</h2>
|
||||||
<span className="text-xs text-dark-50/35">
|
<span className="text-xs text-dark-50/35">
|
||||||
{subscription.is_trial
|
{subscription.is_trial
|
||||||
@@ -117,27 +119,21 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expired info grid */}
|
{/* Expired date */}
|
||||||
<div
|
<div
|
||||||
className="mb-5 flex justify-around rounded-[14px] text-center"
|
className="mb-5 flex items-center justify-center rounded-[14px]"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,59,92,0.04)',
|
background: 'rgba(255,59,92,0.04)',
|
||||||
border: '1px solid rgba(255,59,92,0.08)',
|
border: '1px solid rgba(255,59,92,0.08)',
|
||||||
padding: '16px 18px',
|
padding: '14px 18px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
<div className="mb-0.5 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
|
||||||
{ label: t('dashboard.expired.traffic'), value: '0 GB' },
|
{t('dashboard.expired.expiredDate')}
|
||||||
{ label: t('dashboard.expired.devices'), value: '0' },
|
</div>
|
||||||
{ label: t('dashboard.expired.expiredDate'), value: formattedDate },
|
<div className="ml-3 text-base font-bold tracking-tight text-dark-50/50">
|
||||||
].map((item, i) => (
|
{formattedDate}
|
||||||
<div key={i}>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
|
|||||||
@@ -187,6 +187,25 @@ export default function Subscription() {
|
|||||||
// Extract subscription from response (null if no subscription)
|
// Extract subscription from response (null if no subscription)
|
||||||
const subscription = subscriptionResponse?.subscription ?? null;
|
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({
|
const { data: purchaseOptions, isLoading: optionsLoading } = useQuery({
|
||||||
queryKey: ['purchase-options'],
|
queryKey: ['purchase-options'],
|
||||||
queryFn: subscriptionApi.getPurchaseOptions,
|
queryFn: subscriptionApi.getPurchaseOptions,
|
||||||
@@ -679,7 +698,6 @@ export default function Subscription() {
|
|||||||
const zone = getTrafficZone(usedPercent);
|
const zone = getTrafficZone(usedPercent);
|
||||||
const connectedDevices = devicesData?.total ?? 0;
|
const connectedDevices = devicesData?.total ?? 0;
|
||||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
||||||
const daysLeft = subscription.days_left;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -736,19 +754,6 @@ export default function Subscription() {
|
|||||||
>
|
>
|
||||||
{isUnlimited ? t('dashboard.unlimited') : t(zone.labelKey)}
|
{isUnlimited ? t('dashboard.unlimited') : t(zone.labelKey)}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Plan name */}
|
{/* Plan name */}
|
||||||
@@ -770,11 +775,11 @@ export default function Subscription() {
|
|||||||
color: subscription.is_active ? zone.mainHex : '#FF3B5C',
|
color: subscription.is_active ? zone.mainHex : '#FF3B5C',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{subscription.is_trial
|
{subscription.is_active
|
||||||
? t('subscription.trialStatus')
|
? subscription.is_trial
|
||||||
: subscription.is_active
|
? t('subscription.trialStatus')
|
||||||
? t('subscription.active')
|
: t('subscription.active')
|
||||||
: t('subscription.expired')}
|
: t('subscription.expired')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -992,57 +997,114 @@ export default function Subscription() {
|
|||||||
|
|
||||||
{/* ─── Stats Row ─── */}
|
{/* ─── Stats Row ─── */}
|
||||||
<div className="mb-5 grid grid-cols-2 gap-2.5">
|
<div className="mb-5 grid grid-cols-2 gap-2.5">
|
||||||
{/* Days left */}
|
{/* Countdown timer */}
|
||||||
<div
|
{(() => {
|
||||||
className="rounded-[14px] p-3.5"
|
const isUrgent = countdown.days <= 3;
|
||||||
style={{
|
const isExpired = !subscription.is_active;
|
||||||
background: daysLeft <= 3 ? 'rgba(255,184,0,0.06)' : g.innerBg,
|
return (
|
||||||
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">
|
|
||||||
<div
|
<div
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
className="rounded-[14px] p-3.5"
|
||||||
style={{
|
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
|
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||||
width="13"
|
<div
|
||||||
height="13"
|
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
||||||
viewBox="0 0 24 24"
|
style={{
|
||||||
fill="none"
|
background: isExpired
|
||||||
stroke={daysLeft <= 3 ? '#FFB800' : g.textSecondary}
|
? 'rgba(255,59,92,0.1)'
|
||||||
strokeWidth="2"
|
: isUrgent
|
||||||
strokeLinecap="round"
|
? 'rgba(255,184,0,0.1)'
|
||||||
strokeLinejoin="round"
|
: g.hoverBg,
|
||||||
aria-hidden="true"
|
}}
|
||||||
>
|
>
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
<svg
|
||||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
width="13"
|
||||||
</svg>
|
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>
|
</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 */}
|
{/* Devices */}
|
||||||
<div
|
<div
|
||||||
@@ -1052,7 +1114,7 @@ export default function Subscription() {
|
|||||||
border: `1px solid ${g.innerBorder}`,
|
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
|
<div
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
className="flex h-6 w-6 items-center justify-center rounded-[7px]"
|
||||||
style={{ background: `${zone.mainHex}12` }}
|
style={{ background: `${zone.mainHex}12` }}
|
||||||
@@ -1083,41 +1145,6 @@ export default function Subscription() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* ─── Locations ─── */}
|
{/* ─── Locations ─── */}
|
||||||
|
|||||||
Reference in New Issue
Block a user