import { useState, useEffect, useMemo, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../store/auth'; import { subscriptionApi } from '../api/subscription'; import { referralApi } from '../api/referral'; import { balanceApi } from '../api/balance'; import { wheelApi } from '../api/wheel'; import ConnectionModal from '../components/ConnectionModal'; import Onboarding, { useOnboarding } from '../components/Onboarding'; import PromoOffersSection from '../components/PromoOffersSection'; import { useCurrency } from '../hooks/useCurrency'; // 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, refreshUser } = useAuthStore(); const queryClient = useQueryClient(); const { formatAmount, currencySymbol, formatPositive } = useCurrency(); const [trialError, setTrialError] = useState(null); const [showConnectionModal, setShowConnectionModal] = useState(false); const { isCompleted: isOnboardingCompleted, complete: completeOnboarding } = useOnboarding(); const [showOnboarding, setShowOnboarding] = useState(false); // Refresh user data on mount useEffect(() => { refreshUser(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Fetch balance from API with no caching const { data: balanceData } = useQuery({ queryKey: ['balance'], queryFn: balanceApi.getBalance, staleTime: 0, refetchOnMount: 'always', }); const { data: subscriptionResponse, isLoading: subLoading } = useQuery({ queryKey: ['subscription'], queryFn: subscriptionApi.getSubscription, retry: false, staleTime: 0, refetchOnMount: 'always', }); // Extract subscription from response (null if no subscription) const subscription = subscriptionResponse?.subscription ?? null; const { data: trialInfo, isLoading: trialLoading } = useQuery({ queryKey: ['trial-info'], queryFn: subscriptionApi.getTrialInfo, enabled: !subscription && !subLoading, }); const { data: referralInfo, isLoading: refLoading } = useQuery({ queryKey: ['referral-info'], 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 retry: false, }); const activateTrialMutation = useMutation({ mutationFn: subscriptionApi.activateTrial, onSuccess: () => { setTrialError(null); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['trial-info'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); refreshUser(); }, onError: (error: { response?: { data?: { detail?: string } } }) => { setTrialError(error.response?.data?.detail || t('common.error')); }, }); // Traffic refresh state and mutation const [trafficRefreshCooldown, setTrafficRefreshCooldown] = useState(0); const [trafficData, setTrafficData] = useState<{ traffic_used_gb: number; traffic_used_percent: number; is_unlimited: boolean; } | null>(null); const refreshTrafficMutation = useMutation({ mutationFn: subscriptionApi.refreshTraffic, onSuccess: (data) => { setTrafficData({ traffic_used_gb: data.traffic_used_gb, 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); } else { setTrafficRefreshCooldown(30); } queryClient.invalidateQueries({ queryKey: ['subscription'] }); }, onError: (error: { response?: { status?: number; headers?: { get?: (key: string) => string } }; }) => { if (error.response?.status === 429) { const retryAfter = error.response.headers?.get?.('Retry-After'); setTrafficRefreshCooldown(retryAfter ? parseInt(retryAfter, 10) : 30); } }, }); // Cooldown timer useEffect(() => { if (trafficRefreshCooldown <= 0) return; const timer = setInterval(() => { setTrafficRefreshCooldown((prev) => Math.max(0, prev - 1)); }, 1000); return () => clearInterval(timer); }, [trafficRefreshCooldown]); // Track if we've already triggered auto-refresh this session const hasAutoRefreshed = useRef(false); // Auto-refresh traffic on mount (with 30s caching) useEffect(() => { if (!subscription) return; if (hasAutoRefreshed.current) return; hasAutoRefreshed.current = true; const lastRefresh = localStorage.getItem('traffic_refresh_ts'); const now = Date.now(); const cacheMs = 30 * 1000; if (lastRefresh && now - parseInt(lastRefresh, 10) < cacheMs) { const elapsed = now - parseInt(lastRefresh, 10); const remaining = Math.ceil((cacheMs - elapsed) / 1000); if (remaining > 0) { setTrafficRefreshCooldown(remaining); } return; } refreshTrafficMutation.mutate(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [subscription]); // 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 useEffect(() => { if (!isOnboardingCompleted && !subLoading && !refLoading) { // Small delay to ensure DOM is ready const timer = setTimeout(() => setShowOnboarding(true), 500); return () => clearTimeout(timer); } }, [isOnboardingCompleted, subLoading, refLoading]); // Define onboarding steps based on available data const onboardingSteps = useMemo(() => { type Placement = 'top' | 'bottom' | 'left' | 'right'; const steps: Array<{ target: string; title: string; description: string; placement: Placement; }> = [ { target: 'welcome', title: t('onboarding.steps.welcome.title'), description: t('onboarding.steps.welcome.description'), placement: 'bottom', }, { target: 'balance', title: t('onboarding.steps.balance.title'), description: t('onboarding.steps.balance.description'), placement: 'bottom', }, { target: 'subscription-status', title: t('onboarding.steps.subscription.title'), description: t('onboarding.steps.subscription.description'), placement: 'bottom', }, ]; // Add connect devices step only if subscription exists if (subscription?.subscription_url) { steps.splice(1, 0, { target: 'connect-devices', title: t('onboarding.steps.connectDevices.title'), description: t('onboarding.steps.connectDevices.description'), placement: 'bottom', }); } return steps; }, [t, subscription]); const handleOnboardingComplete = () => { setShowOnboarding(false); completeOnboarding(); }; // Calculate traffic percentage color const getTrafficColor = (percent: number) => { if (percent > 90) return 'bg-error-500'; if (percent > 70) return 'bg-warning-500'; return 'bg-success-500'; }; return (
{/* Header */}

{t('dashboard.welcome', { name: user?.first_name || user?.username || '' })}

{t('dashboard.yourSubscription')}

{/* Subscription Status - Main Card */} {subLoading ? (
{[...Array(4)].map((_, i) => (
))}
) : 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 && ( )}
) : null} {/* 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 ? ( ) : ( {t('subscription.trial.topUpToActivate', 'Top Up Balance')} ) ) : ( )}
)} {/* Promo Offers */} {/* Fortune Wheel Banner */} {wheelConfig?.is_enabled && (
{/* Emoji */} 🎰

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

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

)} {/* Connection Modal */} {showConnectionModal && setShowConnectionModal(false)} />} {/* Onboarding Tutorial */} {showOnboarding && ( )}
); }