diff --git a/src/locales/en.json b/src/locales/en.json index e7490fe..c4fb3cc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -225,7 +225,41 @@ "noActiveSubscription": "No active subscription", "devicesUsed": "Devices: {{used}} of {{total}}", "trafficUsed": "Traffic: {{used}} of {{total}} GB", - "unlimitedTraffic": "Unlimited traffic" + "unlimitedTraffic": "Unlimited traffic", + "zone": { + "normal": "Normal", + "warning": "Moderate", + "danger": "High", + "critical": "Critical" + }, + "connectDevice": "Connect Device", + "devicesConnected": "{{count}} connected", + "trafficUsage": "{{used}} / {{limit}} GB", + "unlimited": "Unlimited", + "tariff": "Tariff", + "validUntil": "until {{date}}", + "daysRemaining": "Days left", + "expired": { + "title": "Subscription Expired", + "trialTitle": "Trial Expired", + "renew": "Renew", + "tariffs": "View Tariffs", + "traffic": "Traffic", + "devices": "Devices", + "expiredDate": "Expired" + }, + "trialOffer": { + "freeTitle": "Free Trial Available", + "paidTitle": "Trial Subscription", + "freeDesc": "Try our VPN for free — no commitment required", + "paidDesc": "Get started with a trial subscription" + }, + "stats": { + "balance": "Balance", + "subscription": "Subscription", + "referrals": "Referrals", + "earnings": "Earnings" + } }, "subscription": { "title": "Subscription", diff --git a/src/locales/ru.json b/src/locales/ru.json index 935ff27..1526c70 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -237,7 +237,41 @@ "noActiveSubscription": "Нет активной подписки", "devicesUsed": "Устройств: {{used}} из {{total}}", "trafficUsed": "Трафик: {{used}} из {{total}} ГБ", - "unlimitedTraffic": "Безлимитный трафик" + "unlimitedTraffic": "Безлимитный трафик", + "zone": { + "normal": "Норма", + "warning": "Умеренный", + "danger": "Высокий", + "critical": "Критический" + }, + "connectDevice": "Подключить устройство", + "devicesConnected": "{{count}} подключено", + "trafficUsage": "{{used}} / {{limit}} ГБ", + "unlimited": "Безлимит", + "tariff": "Тариф", + "validUntil": "до {{date}}", + "daysRemaining": "Дней осталось", + "expired": { + "title": "Подписка истекла", + "trialTitle": "Пробный период истёк", + "renew": "Продлить", + "tariffs": "Тарифы", + "traffic": "Трафик", + "devices": "Устройства", + "expiredDate": "Истекла" + }, + "trialOffer": { + "freeTitle": "Бесплатный пробный период", + "paidTitle": "Пробная подписка", + "freeDesc": "Попробуйте наш VPN бесплатно — без обязательств", + "paidDesc": "Начните с пробной подписки" + }, + "stats": { + "balance": "Баланс", + "subscription": "Подписка", + "referrals": "Рефералы", + "earnings": "Заработок" + } }, "subscription": { "title": "Подписка", diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index fd10941..ce74be9 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useMemo, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Link, useNavigate } from 'react-router'; +import { Link } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../store/auth'; import { useBlockingStore } from '../store/blocking'; @@ -10,54 +10,27 @@ import { balanceApi } from '../api/balance'; import { wheelApi } from '../api/wheel'; import Onboarding, { useOnboarding } from '../components/Onboarding'; import PromoOffersSection from '../components/PromoOffersSection'; -import { HoverBorderGradient } from '../components/ui/hover-border-gradient'; -import { useCurrency } from '../hooks/useCurrency'; +import SubscriptionCardActive from '../components/dashboard/SubscriptionCardActive'; +import SubscriptionCardExpired from '../components/dashboard/SubscriptionCardExpired'; +import TrialOfferCard from '../components/dashboard/TrialOfferCard'; +import StatsGrid from '../components/dashboard/StatsGrid'; import { API } from '../config/constants'; -// Icons -const ArrowRightIcon = () => ( - - - -); - -const SparklesIcon = () => ( - - - -); - const ChevronRightIcon = () => ( ); -const RefreshIcon = ({ className = 'w-4 h-4' }: { className?: string }) => ( - - - -); - export default function Dashboard() { const { t } = useTranslation(); const user = useAuthStore((state) => state.user); const refreshUser = useAuthStore((state) => state.refreshUser); const queryClient = useQueryClient(); - const navigate = useNavigate(); - const { formatAmount, currencySymbol, formatPositive } = useCurrency(); - const [trialError, setTrialError] = useState(null); const { isCompleted: isOnboardingCompleted, complete: completeOnboarding } = useOnboarding(); const [showOnboarding, setShowOnboarding] = useState(false); const blockingType = useBlockingStore((state) => state.blockingType); + const [trialError, setTrialError] = useState(null); // Refresh user data on mount useEffect(() => { @@ -80,7 +53,6 @@ export default function Dashboard() { refetchOnMount: 'always', }); - // Extract subscription from response (null if no subscription) const subscription = subscriptionResponse?.subscription ?? null; const { data: trialInfo, isLoading: trialLoading } = useQuery({ @@ -94,11 +66,10 @@ export default function Dashboard() { queryFn: referralApi.getReferralInfo, }); - // Fetch wheel config to show banner if enabled const { data: wheelConfig } = useQuery({ queryKey: ['wheel-config'], queryFn: wheelApi.getConfig, - staleTime: 60000, // 1 minute + staleTime: 60000, retry: false, }); @@ -132,7 +103,6 @@ export default function Dashboard() { traffic_used_percent: data.traffic_used_percent, is_unlimited: data.is_unlimited, }); - // Save last refresh timestamp to localStorage localStorage.setItem('traffic_refresh_ts', Date.now().toString()); if (data.rate_limited && data.retry_after_seconds) { setTrafficRefreshCooldown(data.retry_after_seconds); @@ -160,10 +130,9 @@ export default function Dashboard() { return () => clearInterval(timer); }, [trafficRefreshCooldown]); - // Track if we've already triggered auto-refresh this session + // Auto-refresh traffic on mount (with 30s caching) const hasAutoRefreshed = useRef(false); - // Auto-refresh traffic on mount (with 30s caching) useEffect(() => { if (!subscription) return; if (hasAutoRefreshed.current) return; @@ -185,20 +154,16 @@ export default function Dashboard() { refreshTrafficMutation.mutate(); }, [subscription, refreshTrafficMutation]); - // User has no subscription if API returns has_subscription: false const hasNoSubscription = subscriptionResponse?.has_subscription === false && !subLoading; // Show onboarding for new users after data loads - // Skip if a blocking screen is active (channel subscription, maintenance, etc.) useEffect(() => { if (!isOnboardingCompleted && !subLoading && !refLoading && !blockingType) { - // Small delay to ensure DOM is ready const timer = setTimeout(() => setShowOnboarding(true), 500); return () => clearTimeout(timer); } }, [isOnboardingCompleted, subLoading, refLoading, blockingType]); - // Define onboarding steps based on available data const onboardingSteps = useMemo(() => { type Placement = 'top' | 'bottom' | 'left' | 'right'; const steps: Array<{ @@ -227,7 +192,6 @@ export default function Dashboard() { }, ]; - // Add connect devices step only if subscription exists if (subscription?.subscription_url) { steps.splice(1, 0, { target: 'connect-devices', @@ -245,13 +209,6 @@ export default function Dashboard() { completeOnboarding(); }; - // Calculate traffic percentage color - const getTrafficColor = (percent: number) => { - if (percent > API.TRAFFIC_CRITICAL_PERCENT) return 'bg-error-500'; - if (percent > API.TRAFFIC_WARN_PERCENT) return 'bg-warning-500'; - return 'bg-success-500'; - }; - return (
{/* Header */} @@ -262,371 +219,59 @@ export default function Dashboard() {

{t('dashboard.yourSubscription')}

- {/* Subscription Status - Main Card */} + {/* Subscription Status Card */} {subLoading ? (
-
-
+
+
-
- {[...Array(4)].map((_, i) => ( -
-
-
-
- ))} -
-
-
-
-
-
-
+
+
+
+
+
+ ) : subscription?.is_expired ? ( + ) : subscription ? ( -
-
-
-

{t('subscription.status')}

- {subscription.tariff_name && ( -
{subscription.tariff_name}
- )} -
- - {subscription.is_trial - ? t('subscription.trialStatus') - : subscription.is_active - ? t('subscription.active') - : t('subscription.expired')} - -
- - {/* Trial Info Banner */} - {subscription.is_trial && subscription.is_active && ( -
-
-
- -
-
-
- {t('subscription.trialBanner.title')} -
-
- {t('subscription.trialBanner.description', { days: subscription.days_left })} -
- - {t('subscription.trialBanner.upgrade')} - - -
-
-
- )} - -
-
-
{t('subscription.expiresAt')}
-
- {new Date(subscription.end_date).toLocaleDateString()} -
-
-
-
- {t('subscription.traffic')} - -
-
- {(trafficData?.traffic_used_gb ?? subscription.traffic_used_gb).toFixed(1)} /{' '} - {subscription.traffic_limit_gb || '∞'} GB -
-
-
-
{t('subscription.devices')}
-
{subscription.device_limit}
-
-
-
{t('subscription.timeLeft')}
-
- {subscription.days_left > 0 - ? t('subscription.days', { count: subscription.days_left }) - : `${t('subscription.hours', { count: subscription.hours_left })} ${t('subscription.minutes', { count: subscription.minutes_left })}`} -
-
-
- - {/* Traffic Progress */} - {subscription.traffic_limit_gb > 0 && ( -
-
- {t('subscription.trafficUsed')} - - {(trafficData?.traffic_used_percent ?? subscription.traffic_used_percent).toFixed( - 1, - )} - % - -
-
-
-
-
- )} - -
- - {t('dashboard.viewSubscription')} - - {subscription.subscription_url && ( - navigate('/connection')} - containerClassName="w-full" - className="flex w-full items-center justify-center py-2.5 text-sm" - data-onboarding="connect-devices" - > - {t('subscription.getConfig')} - - )} -
-
+ ) : null} - {/* Promo Offers - показываем сразу после карточки подписки */} - - - {/* Stats Grid */} -
- {/* Balance */} - -
- {t('balance.currentBalance')} - - - -
-
- {formatAmount(balanceData?.balance_rubles || 0)} - {currencySymbol} -
- - - {/* Subscription */} - -
- {t('subscription.title')} - - - -
- {subLoading ? ( -
- ) : subscription ? ( -
- {subscription.days_left > 0 ? ( - <> - {subscription.days_left} - {t('subscription.days')} - - ) : subscription.hours_left > 0 ? ( - <> - {subscription.hours_left} - {t('subscription.hours')} - - ) : subscription.minutes_left > 0 ? ( - <> - {subscription.minutes_left} - {t('subscription.minutes')} - - ) : ( - {t('subscription.expired')} - )} -
- ) : ( -
{t('subscription.inactive')}
- )} - - - {/* Referrals */} - -
- {t('referral.stats.totalReferrals')} - - - -
- {refLoading ? ( -
- ) : ( -
{referralInfo?.total_referrals || 0}
- )} - - - {/* Earnings */} - -
- {t('referral.stats.totalEarnings')} - - - -
- {refLoading ? ( -
- ) : ( -
- {formatPositive(referralInfo?.total_earnings_rubles || 0)} -
- )} - -
- {/* Trial Activation */} {hasNoSubscription && !trialLoading && trialInfo?.is_available && ( -
-
-
- -
-
-

- {trialInfo.requires_payment - ? t('subscription.trial.titlePaid', 'Trial Subscription') - : t('subscription.trial.title', 'Free Trial')} -

-

- {t('subscription.trial.description', 'Try our VPN service for free!')} -

- -
-
-
- {trialInfo.duration_days} -
-
{t('subscription.trial.days')}
-
-
-
- {trialInfo.traffic_limit_gb || '∞'} -
-
GB
-
-
-
{trialInfo.device_limit}
-
{t('subscription.trial.devices')}
-
-
- - {trialInfo.requires_payment && trialInfo.price_rubles > 0 && ( -
-
- - {t('subscription.trial.price', 'Activation price')}: - - - {trialInfo.price_rubles.toFixed(2)} {currencySymbol} - -
-
- - {t('balance.currentBalance', 'Your balance')}: - - = trialInfo.price_kopeks ? 'text-success-400' : 'text-warning-400'}`} - > - {formatAmount(balanceData?.balance_rubles || 0)} {currencySymbol} - -
- {(balanceData?.balance_kopeks || 0) < trialInfo.price_kopeks && ( -
- {t( - 'subscription.trial.insufficientBalance', - 'Top up your balance to activate', - )} -
- )} -
- )} - - {trialError && ( -
- {trialError} -
- )} - - {trialInfo.requires_payment && trialInfo.price_kopeks > 0 ? ( - (balanceData?.balance_kopeks || 0) >= trialInfo.price_kopeks ? ( - activateTrialMutation.mutate()} - containerClassName={`w-full ${activateTrialMutation.isPending ? 'pointer-events-none opacity-50' : ''}`} - className="flex w-full items-center justify-center" - > - {activateTrialMutation.isPending - ? t('common.loading', 'Loading...') - : t('subscription.trial.payAndActivate', 'Pay from Balance & Activate')} - - ) : ( - - {t('subscription.trial.topUpToActivate', 'Top Up Balance')} - - ) - ) : ( - activateTrialMutation.mutate()} - containerClassName={`w-full ${activateTrialMutation.isPending ? 'pointer-events-none opacity-50' : ''}`} - className="flex w-full items-center justify-center" - > - {activateTrialMutation.isPending - ? t('common.loading', 'Loading...') - : t('subscription.trial.activate', 'Activate Free Trial')} - - )} -
-
-
+ )} + {/* Promo Offers */} + + + {/* Stats Grid */} + + {/* Fortune Wheel Banner */} {wheelConfig?.is_enabled && (
- {/* Emoji */} 🎰

{t('wheel.banner.title')}

diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 4395da6..9356176 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -6,6 +6,7 @@ import { AxiosError } from 'axios'; import { subscriptionApi } from '../api/subscription'; import { promoApi } from '../api/promo'; import { HoverBorderGradient } from '../components/ui/hover-border-gradient'; +import TrafficProgressBar from '../components/dashboard/TrafficProgressBar'; import type { PurchaseSelection, PeriodOption, @@ -605,12 +606,6 @@ export default function Subscription() { } }; - const getTrafficColor = (percent: number) => { - if (percent > 90) return 'bg-error-500'; - if (percent > 70) return 'bg-warning-500'; - return 'bg-success-500'; - }; - const toggleServer = (uuid: string) => { if (selectedServers.includes(uuid)) { if (selectedServers.length > 1) { @@ -756,11 +751,11 @@ export default function Subscription() { {t('subscription.connectionInfo')}
- {/* Get Config Button */} + {/* Get Config Button with device indicator */} navigate('/connection')} containerClassName="mb-3 w-full" - className="flex w-full items-center justify-center gap-2 py-3" + className="flex w-full items-center justify-center gap-3 py-3" > - {t('subscription.getConfig')} + {t('subscription.getConfig')} +
+ {Array.from({ length: Math.min(subscription.device_limit, 5) }).map((_, i) => ( +
+ ))} +
+ {/* Compact traffic progress bar */} +
+ +
+ {/* Subscription URL - hidden when hide_subscription_link is true */} {!subscription.hide_subscription_link && (
@@ -885,21 +898,20 @@ export default function Subscription() {
{t('subscription.trafficUsed')} - + {(trafficData?.traffic_used_percent ?? subscription.traffic_used_percent).toFixed( 1, )} %
-
-
-
+
)}