feat: блокировка кнопки устройств при лимите + убраны дубли трафика

- Кнопка «Подключить устройство» заблокирована при достижении лимита
- Haptic feedback при нажатии на заблокированную кнопку
- Предупреждение «Отключите устройства для подключения новых»
- Убран дублирующийся текст трафика под прогресс-баром (безлимит)
This commit is contained in:
Fringg
2026-03-18 04:21:34 +03:00
parent 246fafdbfc
commit 8636bd7659
8 changed files with 35 additions and 26 deletions

View File

@@ -10,6 +10,7 @@ import { useTrafficZone } from '../../hooks/useTrafficZone';
import { formatTraffic } from '../../utils/formatTraffic'; import { formatTraffic } from '../../utils/formatTraffic';
import { getGlassColors } from '../../utils/glassTheme'; import { getGlassColors } from '../../utils/glassTheme';
import { HoverBorderGradient } from '../ui/hover-border-gradient'; import { HoverBorderGradient } from '../ui/hover-border-gradient';
import { useHaptic } from '../../platform';
import type { Subscription } from '../../types'; import type { Subscription } from '../../types';
interface SubscriptionCardActiveProps { interface SubscriptionCardActiveProps {
@@ -58,6 +59,10 @@ export default function SubscriptionCardActive({
const isUnlimited = trafficData?.is_unlimited ?? subscription.traffic_limit_gb === 0; const isUnlimited = trafficData?.is_unlimited ?? subscription.traffic_limit_gb === 0;
const zone = useTrafficZone(usedPercent); const zone = useTrafficZone(usedPercent);
const animatedPercent = useAnimatedNumber(usedPercent); const animatedPercent = useAnimatedNumber(usedPercent);
const haptic = useHaptic();
const isAtDeviceLimit =
subscription.device_limit > 0 && connectedDevices >= subscription.device_limit;
const formattedDate = new Date(subscription.end_date).toLocaleDateString(); const formattedDate = new Date(subscription.end_date).toLocaleDateString();
const daysLeft = subscription.days_left; const daysLeft = subscription.days_left;
@@ -203,8 +208,15 @@ export default function SubscriptionCardActive({
<HoverBorderGradient <HoverBorderGradient
as="button" as="button"
accentColor={zone.mainHex} accentColor={zone.mainHex}
onClick={() => navigate('/connection')} disabled={isAtDeviceLimit}
className="mb-2.5 flex w-full items-center gap-3.5 rounded-[14px] p-3.5 text-left transition-shadow duration-300" onClick={() => {
if (isAtDeviceLimit) {
haptic.notification('error');
return;
}
navigate('/connection');
}}
className={`mb-2.5 flex w-full items-center gap-3.5 rounded-[14px] p-3.5 text-left transition-shadow duration-300${isAtDeviceLimit ? 'cursor-not-allowed opacity-50' : ''}`}
data-onboarding="connect-devices" data-onboarding="connect-devices"
style={{ fontFamily: 'inherit' }} style={{ fontFamily: 'inherit' }}
> >
@@ -243,6 +255,14 @@ export default function SubscriptionCardActive({
max: subscription.device_limit, max: subscription.device_limit,
})} })}
</div> </div>
{isAtDeviceLimit && (
<div
className="mt-1 text-[10px] font-medium"
style={{ color: 'rgb(var(--color-warning-400))' }}
>
{t('dashboard.deviceLimitReached')}
</div>
)}
</div> </div>
{/* Device indicator */} {/* Device indicator */}

View File

@@ -16,7 +16,7 @@ interface TrafficProgressBarProps {
const THRESHOLDS = [50, 75, 90]; const THRESHOLDS = [50, 75, 90];
export default function TrafficProgressBar({ export default function TrafficProgressBar({
usedGb, usedGb: _usedGb,
limitGb, limitGb,
percent, percent,
isUnlimited, isUnlimited,
@@ -83,29 +83,6 @@ export default function TrafficProgressBar({
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>
{/* Below bar: label + usage */}
{!compact && (
<div className="mt-2 flex items-center justify-between px-0.5">
<span
className="flex items-center gap-1.5 text-[11px] font-semibold"
style={{ color: zone.mainVar }}
>
<span
className="inline-block h-1.5 w-1.5 animate-unlimited-pulse rounded-full"
style={{
background: zone.mainVar,
boxShadow: `0 0 8px ${zone.mainVar}`,
}}
aria-hidden="true"
/>
{t('dashboard.unlimitedTraffic')}
</span>
<span className="font-mono text-[11px] text-dark-50/30">
{t('dashboard.usedTraffic', { amount: formatTraffic(usedGb) })}
</span>
</div>
)}
</div> </div>
); );
} }

View File

@@ -8,6 +8,8 @@ interface HoverBorderGradientProps extends React.HTMLAttributes<HTMLElement> {
accentColor?: string; accentColor?: string;
/** Full rotation duration in seconds. Default: 3 */ /** Full rotation duration in seconds. Default: 3 */
duration?: number; duration?: number;
/** Disables the element (applies to interactive elements like buttons) */
disabled?: boolean;
} }
/** /**

View File

@@ -254,6 +254,7 @@
"daysRemaining": "Days left", "daysRemaining": "Days left",
"usageLast14Days": "Usage last 14 days", "usageLast14Days": "Usage last 14 days",
"maxUsage": "max {{amount}}", "maxUsage": "max {{amount}}",
"deviceLimitReached": "Disconnect devices to add new ones",
"expired": { "expired": {
"title": "Subscription Expired", "title": "Subscription Expired",
"trialTitle": "Trial Expired", "trialTitle": "Trial Expired",

View File

@@ -242,6 +242,7 @@
"devices": "دستگاه‌ها", "devices": "دستگاه‌ها",
"expiredDate": "منقضی شده" "expiredDate": "منقضی شده"
}, },
"deviceLimitReached": "برای افزودن دستگاه جدید، دستگاه‌های فعلی را قطع کنید",
"suspended": { "suspended": {
"title": "اشتراک معلق شده", "title": "اشتراک معلق شده",
"resume": "از سرگیری" "resume": "از سرگیری"

View File

@@ -266,6 +266,7 @@
"daysRemaining": "Дней осталось", "daysRemaining": "Дней осталось",
"usageLast14Days": "Расход за 14 дней", "usageLast14Days": "Расход за 14 дней",
"maxUsage": "макс {{amount}}", "maxUsage": "макс {{amount}}",
"deviceLimitReached": "Отключите устройства для подключения новых",
"expired": { "expired": {
"title": "Подписка истекла", "title": "Подписка истекла",
"trialTitle": "Пробный период истёк", "trialTitle": "Пробный период истёк",

View File

@@ -242,6 +242,7 @@
"devices": "设备", "devices": "设备",
"expiredDate": "过期时间" "expiredDate": "过期时间"
}, },
"deviceLimitReached": "断开设备以添加新设备",
"suspended": { "suspended": {
"title": "订阅已暂停", "title": "订阅已暂停",
"resume": "恢复" "resume": "恢复"

View File

@@ -35,6 +35,12 @@
box-shadow: 0 0 20px var(--accent-glow, rgba(var(--color-accent-400), 0.25)); box-shadow: 0 0 20px var(--accent-glow, rgba(var(--color-accent-400), 0.25));
} }
.hover-border-gradient:disabled,
.hover-border-gradient[disabled] {
box-shadow: none;
pointer-events: auto;
}
.light .hover-border-gradient { .light .hover-border-gradient {
--_bg: var(--border-inner-bg, rgb(var(--color-champagne-100))); --_bg: var(--border-inner-bg, rgb(var(--color-champagne-100)));
} }