Files
bedolaga-cabinet/src/components/dashboard/ConnectDeviceTile.tsx
Fringg 39b9b8e1d2 fix(dashboard): счётчик устройств в плитке на главной
Дополнения к PR #544 при мерже.

Плитка на главной берёт число подключённых устройств из запроса ['devices'],
а он выключен ровно там, где плитка живёт: `enabled: !!subscription &&
!isMultiTariff`, тогда как блок с плиткой рендерится в мультитарифной ветке,
где `subscription` всегда null (свой запрос отключён по !isMultiTariff).
То есть счётчик показывал бы «0 из N» всегда, лимит устройств не срабатывал бы
никогда, а точки-индикаторы стояли бы пустыми — при том, что счётчик и есть
смысл плитки.

Добавлен запрос по конкретной подписке: getDevices принимает subscription_id,
ключ ['devices', id] — тот же, что на странице подписки, поэтому кэш общий и
лишнего похода в сеть при переходе туда не будет. Заодно вынес выбор
единственной подписки в переменную, чтобы условие показа и данные плитки не
разъезжались.

Отдельно поправлен потерянный пробел в className: `duration-300${...}` склеивал
класс в `duration-300cursor-not-allowed`, из-за чего оформление заблокированной
плитки не применялось. Баг приехал вместе с переносом из SubscriptionCardActive
(там он с самого начала), но раз компонент всё равно выделяется — чиню здесь.
2026-08-07 15:27:17 +03:00

144 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { useHaptic } from '../../platform';
import { useTheme } from '../../hooks/useTheme';
import { useTrafficZone } from '../../hooks/useTrafficZone';
import { getGlassColors } from '../../utils/glassTheme';
import { HoverBorderGradient } from '../ui/hover-border-gradient';
interface ConnectDeviceTileProps {
subscription: {
id: number;
device_limit: number;
subscription_url?: string | null;
};
connectedDevices: number;
/** Процент израсходованного трафика — от него зависит акцентный цвет плитки. */
usedPercent?: number;
}
/**
* Плитка «Подключить устройство».
*
* Живёт и в карточке активной подписки, и на главной: пользователь,
* которому подписку выдал бонус рекламной кампании, попадает на главную
* с готовым доступом — и без этой плитки не понимает, что делать дальше.
*/
export default function ConnectDeviceTile({
subscription,
connectedDevices,
usedPercent = 0,
}: ConnectDeviceTileProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const haptic = useHaptic();
const { isDark } = useTheme();
const g = getGlassColors(isDark);
const zone = useTrafficZone(usedPercent);
const isAtDeviceLimit =
subscription.device_limit > 0 && connectedDevices >= subscription.device_limit;
if (!subscription.subscription_url) return null;
return (
<HoverBorderGradient
as="button"
accentColor={zone.mainHex}
disabled={isAtDeviceLimit}
onClick={() => {
if (isAtDeviceLimit) {
haptic.notification('error');
return;
}
navigate(`/connection?sub=${subscription.id}`);
}}
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"
style={{ fontFamily: 'inherit' }}
>
{/* Monitor icon */}
<div
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] transition-colors duration-500"
style={{ background: `rgba(${zone.mainVarRaw}, 0.07)` }}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke={zone.mainVar}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M12 17v4M8 21h8" />
<path d="M12 8v4M10 10h4" opacity="0.7" />
</svg>
</div>
{/* Text */}
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold tracking-tight text-dark-50">
{t('dashboard.connectDevice')}
</div>
<div className="mt-0.5 text-[11px] text-dark-50/30">
{subscription.device_limit === 0
? t('dashboard.devicesConnectedUnlimited', { used: connectedDevices })
: t('dashboard.devicesOfMax', {
used: connectedDevices,
max: subscription.device_limit,
})}
</div>
{isAtDeviceLimit && (
<div
className="mt-1 text-[10px] font-medium"
style={{ color: 'rgb(var(--color-warning-400))' }}
>
{t('dashboard.deviceLimitReached')}
</div>
)}
</div>
{/* Device indicator */}
{subscription.device_limit === 0 ? (
<div className="flex flex-shrink-0 items-center text-lg text-dark-50/40" aria-hidden="true">
</div>
) : subscription.device_limit <= 10 ? (
<div className="flex flex-shrink-0 gap-1.5" aria-hidden="true">
{Array.from({ length: subscription.device_limit }, (_, i) => (
<div
key={i}
className="h-[7px] w-[7px] rounded-full transition-all duration-300"
style={{
background: i < connectedDevices ? zone.mainVar : g.textGhost,
boxShadow: i < connectedDevices ? `0 0 6px rgba(${zone.mainVarRaw}, 0.31)` : 'none',
}}
/>
))}
</div>
) : (
<div className="flex w-16 flex-shrink-0 items-center" aria-hidden="true">
<div
className="h-[6px] w-full overflow-hidden rounded-full"
style={{ background: g.textGhost }}
>
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${Math.round((connectedDevices / subscription.device_limit) * 100)}%`,
background: zone.mainVar,
boxShadow: `0 0 8px rgba(${zone.mainVarRaw}, 0.25)`,
minWidth: connectedDevices > 0 ? '4px' : '0px',
}}
/>
</div>
</div>
)}
</HoverBorderGradient>
);
}