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')} refreshTrafficMutation.mutate()} disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0} className="rounded-full p-1 text-dark-400 transition-colors hover:bg-dark-700/50 hover:text-accent-400 disabled:cursor-not-allowed disabled:opacity-50" title={ trafficRefreshCooldown > 0 ? `${trafficRefreshCooldown}s` : t('common.refresh') } > {(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 && ( setShowConnectionModal(true)} className="btn-secondary py-2.5 text-sm" data-onboarding="connect-devices" > {t('subscription.getConfig')} )} ) : 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 ? ( activateTrialMutation.mutate()} disabled={activateTrialMutation.isPending} className="btn-primary w-full" > {activateTrialMutation.isPending ? t('common.loading', 'Loading...') : t('subscription.trial.payAndActivate', 'Pay from Balance & Activate')} ) : ( {t('subscription.trial.topUpToActivate', 'Top Up Balance')} ) ) : ( activateTrialMutation.mutate()} disabled={activateTrialMutation.isPending} className="btn-primary w-full" > {activateTrialMutation.isPending ? t('common.loading', 'Loading...') : t('subscription.trial.activate', 'Activate Free Trial')} )} )} {/* 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 && ( )} ); }
{t('dashboard.yourSubscription')}
{t('subscription.trial.description', 'Try our VPN service for free!')}
{t('wheel.banner.description')}