mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-14 08:43:08 +00:00
feat(dashboard): подключение устройства прикреплено к карточке подписки
На главной подключение жило отдельной плиткой под карточкой, и показывалось только когда подписка одна: при нескольких «выбор устройства неоднозначен». Получалось три плитки-ровни подряд — подписка, действие над этой подпиской и покупка новой, — из которых вторая ровней не была. При нескольких подписках подключение с главной пропадало вовсе. Теперь это подвал самой карточки: тело ведёт в подписку, подвал — в подключение. Одна карточка на подписку при любом их числе, отдельных плиток нет. Карточка снаружи перестала быть `<button>` — вложенные кнопки невалидны и ведут себя в браузерах непредсказуемо, поэтому это обёртка с двумя зонами нажатия. Подвал включается пропом `connect` и задан только на главной: список «Все подписки» остаётся перечнем без действий. Где подвал есть, из строки статистики убран счётчик лимита устройств — он дублировал бы счётчик подвала. Акцентом подсвечивается только подписка без единого устройства: там действие и нужно. У остальных подвал приглушён и не спорит с карточкой. Исчерпанный лимит не блокирует кнопку, а ведёт в разбор: сколько слотов даёт тариф, какие устройства их занимают, отключить лишнее либо добавить слоты. Докупка скрыта на тестовой подписке и при безлимите — те же условия, что у блока докупки на странице подписки, — и тогда текст не обещает того, чего сделать нельзя. Пока счётчик устройств не пришёл, подвал показывает скелетон, а не догадку о состоянии. Его высоту задаёт невидимый спан с той же типографикой: подбор числом даёт дробные пиксели строчного бокса, и карточка дёргается. Проверено замером — сдвиг 0px на всех состояниях. ResponsiveSheet: на телефоне нижний шит, с 640px обычное центрированное окно. Шит на десктопе — плашка у нижнего края с ручкой перетаскивания, бессмысленной для мыши. Компонент общий, остальные шиты кабинета смогут его переиспользовать. ConnectDeviceTile не тронут и продолжает жить в карточке подписки одиночного тарифа вместе с якорем онбординга. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQueries, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
@@ -14,13 +14,13 @@ import PromoOffersSection from '../components/PromoOffersSection';
|
||||
import NewsSection from '../components/news/NewsSection';
|
||||
import SubscriptionCardActive from '../components/dashboard/SubscriptionCardActive';
|
||||
import SubscriptionCardExpired from '../components/dashboard/SubscriptionCardExpired';
|
||||
import ConnectDeviceTile from '../components/dashboard/ConnectDeviceTile';
|
||||
import TrialOfferCard from '../components/dashboard/TrialOfferCard';
|
||||
import StatsGrid from '../components/dashboard/StatsGrid';
|
||||
import { giftApi } from '../api/gift';
|
||||
import { promoApi } from '../api/promo';
|
||||
import PendingGiftCard from '../components/dashboard/PendingGiftCard';
|
||||
import SubscriptionListCard from '../components/subscription/SubscriptionListCard';
|
||||
import { DeviceLimitSheet } from '../components/subscription/DeviceLimitSheet';
|
||||
import { API } from '../config/constants';
|
||||
import { ChevronRightIcon, StarIcon } from '@/components/icons';
|
||||
|
||||
@@ -86,18 +86,29 @@ export default function Dashboard() {
|
||||
// показывал бы «0 из N», а лимит устройств не срабатывал бы никогда — то
|
||||
// есть ровно то, ради чего плитку и добавили, не работало бы.
|
||||
// Ключ ['devices', id] — тот же, что на странице подписки, так что кэш общий.
|
||||
const homeSingleSub =
|
||||
isMultiTariff && multiSubData?.subscriptions?.length === 1
|
||||
? multiSubData.subscriptions[0]
|
||||
: null;
|
||||
// Карточки подписок на главной показывают, сколько устройств подключено, и
|
||||
// дают подключить ещё. Число устройств живёт в панели, поэтому запрос идёт
|
||||
// на каждую показанную подписку; ключ ['devices', id] тот же, что на
|
||||
// странице подписки, так что кэш общий и переход туда не стоит сети.
|
||||
const visibleSubscriptions = useMemo(
|
||||
() => multiSubData?.subscriptions?.slice(0, 3) ?? [],
|
||||
[multiSubData],
|
||||
);
|
||||
|
||||
const { data: homeSingleSubDevices } = useQuery({
|
||||
queryKey: ['devices', homeSingleSub?.id],
|
||||
queryFn: () => subscriptionApi.getDevices(homeSingleSub?.id),
|
||||
enabled: !!homeSingleSub,
|
||||
staleTime: API.BALANCE_STALE_TIME_MS,
|
||||
const deviceQueries = useQueries({
|
||||
queries: visibleSubscriptions.map((sub) => ({
|
||||
queryKey: ['devices', sub.id],
|
||||
queryFn: () => subscriptionApi.getDevices(sub.id),
|
||||
staleTime: API.BALANCE_STALE_TIME_MS,
|
||||
})),
|
||||
});
|
||||
|
||||
// Подписка, у которой разбираем исчерпанный лимит устройств.
|
||||
const [deviceLimitSubId, setDeviceLimitSubId] = useState<number | null>(null);
|
||||
const deviceLimitSub = visibleSubscriptions.find((s) => s.id === deviceLimitSubId) ?? null;
|
||||
const deviceLimitDevices =
|
||||
deviceQueries[visibleSubscriptions.findIndex((s) => s.id === deviceLimitSubId)]?.data;
|
||||
|
||||
const { data: referralInfo, isLoading: refLoading } = useQuery({
|
||||
queryKey: ['referral-info'],
|
||||
queryFn: referralApi.getReferralInfo,
|
||||
@@ -313,33 +324,18 @@ export default function Dashboard() {
|
||||
{t('dashboard.manageAll', 'Управление')} →
|
||||
</Link>
|
||||
</div>
|
||||
{multiSubData.subscriptions.slice(0, 3).map((sub) => (
|
||||
{visibleSubscriptions.map((sub, index) => (
|
||||
<SubscriptionListCard
|
||||
key={sub.id}
|
||||
subscription={sub}
|
||||
onClick={() => navigate(`/subscriptions/${sub.id}`)}
|
||||
connect={{
|
||||
connectedDevices: deviceQueries[index]?.data?.total,
|
||||
onConnect: () => navigate(`/connection?sub=${sub.id}`),
|
||||
onManage: () => setDeviceLimitSubId(sub.id),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{/* Подписку мог выдать бонус рекламной кампании — она создаётся сама,
|
||||
и человек попадает на главную с готовым доступом. Пока подписка
|
||||
одна, показываем здесь же, как подключить устройство: иначе за
|
||||
этим нужно уходить на отдельную страницу, о чём он не догадается. */}
|
||||
{homeSingleSub && (
|
||||
<ConnectDeviceTile
|
||||
subscription={homeSingleSub}
|
||||
connectedDevices={homeSingleSubDevices?.total ?? 0}
|
||||
usedPercent={
|
||||
homeSingleSub.traffic_limit_gb > 0
|
||||
? Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
(homeSingleSub.traffic_used_gb / homeSingleSub.traffic_limit_gb) * 100,
|
||||
),
|
||||
)
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{multiSubData.subscriptions.length > 3 && (
|
||||
<Link
|
||||
to="/subscriptions"
|
||||
@@ -465,6 +461,22 @@ export default function Dashboard() {
|
||||
onSkip={handleOnboardingComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deviceLimitSub && (
|
||||
<DeviceLimitSheet
|
||||
isOpen
|
||||
onClose={() => setDeviceLimitSubId(null)}
|
||||
subscriptionId={deviceLimitSub.id}
|
||||
subscriptionName={deviceLimitSub.tariff_name || t('subscription.defaultName', 'Подписка')}
|
||||
deviceLimit={deviceLimitSub.device_limit}
|
||||
isTrial={deviceLimitSub.is_trial}
|
||||
devices={deviceLimitDevices?.devices ?? []}
|
||||
onOpenSubscription={() => {
|
||||
setDeviceLimitSubId(null);
|
||||
navigate(`/subscriptions/${deviceLimitSub.id}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user