import { useEffect, useState, useCallback, useRef } from 'react'; import { useLocation, useNavigate, Link } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { AnimatePresence, motion } from 'framer-motion'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '@/store/auth'; import { useBackButton, useHaptic } from '@/platform'; import { useTelegramWebApp } from '@/hooks/useTelegramWebApp'; import { referralApi } from '@/api/referral'; import { wheelApi } from '@/api/wheel'; import { contestsApi } from '@/api/contests'; import { pollsApi } from '@/api/polls'; import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, isLogoPreloaded, } from '@/api/branding'; import { setCachedFullscreenEnabled, isTelegramMobile } from '@/hooks/useTelegramWebApp'; import { cn } from '@/lib/utils'; import WebSocketNotifications from '@/components/WebSocketNotifications'; import SuccessNotificationModal from '@/components/SuccessNotificationModal'; import LanguageSwitcher from '@/components/LanguageSwitcher'; import TicketNotificationBell from '@/components/TicketNotificationBell'; import { MobileBottomNav } from './MobileBottomNav'; import { AppHeader } from './AppHeader'; import { MovingGradient } from './MovingGradient'; import { slideUp, slideUpTransition } from '@/components/motion/transitions'; // Desktop nav icons const HomeIcon = ({ className }: { className?: string }) => ( ); const CreditCardIcon = ({ className }: { className?: string }) => ( ); const ChatIcon = ({ className }: { className?: string }) => ( ); const UserIcon = ({ className }: { className?: string }) => ( ); const UsersIcon = ({ className }: { className?: string }) => ( ); const ShieldIcon = ({ className }: { className?: string }) => ( ); const LogoutIcon = ({ className }: { className?: string }) => ( ); const FALLBACK_NAME = import.meta.env.VITE_APP_NAME || 'Cabinet'; const FALLBACK_LOGO = import.meta.env.VITE_APP_LOGO || 'V'; interface AppShellProps { children: React.ReactNode; } export function AppShell({ children }: AppShellProps) { const { t } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); const { isAdmin, isAuthenticated, logout } = useAuthStore(); const { isFullscreen, safeAreaInset, contentSafeAreaInset, requestFullscreen, isTelegramWebApp } = useTelegramWebApp(); const haptic = useHaptic(); // Only apply fullscreen UI adjustments on mobile Telegram (iOS/Android) const isMobileFullscreen = isFullscreen && isTelegramMobile(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [isKeyboardOpen, setIsKeyboardOpen] = useState(false); // Scroll position restoration for admin pages const scrollPositions = useRef>({}); // Disable browser's automatic scroll restoration useEffect(() => { if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; } }, []); // Continuously save scroll position for current path useEffect(() => { const currentPath = location.pathname; // Only track scroll for admin pages if (!currentPath.startsWith('/admin')) return; const handleScroll = () => { scrollPositions.current[currentPath] = window.scrollY; }; // Save on scroll window.addEventListener('scroll', handleScroll, { passive: true }); // Restore scroll position immediately (synchronous) const savedPosition = scrollPositions.current[currentPath]; if (savedPosition !== undefined && savedPosition > 0) { // Immediate restore window.scrollTo({ top: savedPosition, behavior: 'instant' }); } return () => { window.removeEventListener('scroll', handleScroll); }; }, [location.pathname]); // Branding const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: async () => { const data = await brandingApi.getBranding(); setCachedBranding(data); preloadLogo(data); return data; }, initialData: getCachedBranding() ?? undefined, staleTime: 60000, enabled: isAuthenticated, }); const appName = branding ? branding.name : FALLBACK_NAME; const logoLetter = branding?.logo_letter || FALLBACK_LOGO; const hasCustomLogo = branding?.has_custom_logo || false; const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null; // Set document title useEffect(() => { document.title = appName || 'VPN'; }, [appName]); // Update favicon useEffect(() => { if (!logoUrl) return; const link = document.querySelector("link[rel*='icon']") || document.createElement('link'); link.type = 'image/x-icon'; link.rel = 'shortcut icon'; link.href = logoUrl; document.head.appendChild(link); }, [logoUrl]); // Fullscreen setting from server const { data: fullscreenSetting } = useQuery({ queryKey: ['fullscreen-enabled'], queryFn: brandingApi.getFullscreenEnabled, staleTime: 60000, }); // Apply fullscreen setting when loaded from server // Only apply on mobile Telegram (iOS/Android) - desktop doesn't need fullscreen useEffect(() => { if (!fullscreenSetting || !isTelegramWebApp) return; // Update cache for future app starts setCachedFullscreenEnabled(fullscreenSetting.enabled); // Request fullscreen if enabled, not already fullscreen, and on mobile Telegram if (fullscreenSetting.enabled && !isFullscreen && isTelegramMobile()) { requestFullscreen(); } }, [fullscreenSetting, isTelegramWebApp, isFullscreen, requestFullscreen]); // Feature flags const { data: referralTerms } = useQuery({ queryKey: ['referral-terms'], queryFn: referralApi.getReferralTerms, enabled: isAuthenticated, staleTime: 60000, retry: false, }); const { data: wheelConfig } = useQuery({ queryKey: ['wheel-config'], queryFn: wheelApi.getConfig, enabled: isAuthenticated, staleTime: 60000, retry: false, }); const { data: contestsCount } = useQuery({ queryKey: ['contests-count'], queryFn: contestsApi.getCount, enabled: isAuthenticated, staleTime: 60000, retry: false, }); const { data: pollsCount } = useQuery({ queryKey: ['polls-count'], queryFn: pollsApi.getCount, enabled: isAuthenticated, staleTime: 60000, retry: false, }); // BackButton for Telegram Mini App // Don't show back button on main tab pages (bottom nav) - users navigate via tabs const mainTabPaths = ['/', '/subscription', '/balance', '/referral', '/support']; const isMainTabPage = mainTabPaths.includes(location.pathname); const handleBack = useCallback(() => { if (mobileMenuOpen) { setMobileMenuOpen(false); return; } navigate(-1); }, [mobileMenuOpen, navigate]); useBackButton(isMainTabPage ? null : handleBack); // Keyboard detection for hiding bottom nav useEffect(() => { const handleFocusIn = (e: FocusEvent) => { const target = e.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { setIsKeyboardOpen(true); } }; const handleFocusOut = (e: FocusEvent) => { const relatedTarget = e.relatedTarget as HTMLElement | null; if ( !relatedTarget || (relatedTarget.tagName !== 'INPUT' && relatedTarget.tagName !== 'TEXTAREA' && !relatedTarget.isContentEditable) ) { setIsKeyboardOpen(false); } }; document.addEventListener('focusin', handleFocusIn); document.addEventListener('focusout', handleFocusOut); return () => { document.removeEventListener('focusin', handleFocusIn); document.removeEventListener('focusout', handleFocusOut); }; }, []); // Desktop navigation items const desktopNavItems = [ { path: '/', label: t('nav.dashboard'), icon: HomeIcon }, { path: '/balance', label: t('nav.balance'), icon: CreditCardIcon }, { path: '/support', label: t('nav.support'), icon: ChatIcon }, { path: '/profile', label: t('nav.profile'), icon: UserIcon }, ]; const isActive = (path: string) => { if (path === '/') return location.pathname === '/'; return location.pathname.startsWith(path); }; const handleNavClick = () => { haptic.impact('light'); }; // Calculate header height based on fullscreen mode (only on mobile Telegram) const headerHeight = isMobileFullscreen ? 64 + Math.max(safeAreaInset.top, contentSafeAreaInset.top) + 45 : 64; return (
{/* Animated background */} {/* Global components */} {/* Desktop Header */}
{/* Logo */}
{logoLetter} {hasCustomLogo && logoUrl && ( {appName )}
{appName} {/* Center Navigation */} {/* Right side actions */}
{/* Mobile Header */} {}} headerHeight={headerHeight} isFullscreen={isMobileFullscreen} safeAreaInset={safeAreaInset} contentSafeAreaInset={contentSafeAreaInset} wheelEnabled={wheelConfig?.is_enabled} referralEnabled={referralTerms?.is_enabled} hasContests={(contestsCount?.count ?? 0) > 0} hasPolls={(pollsCount?.count ?? 0) > 0} /> {/* Desktop spacer */}
{/* Mobile spacer */}
{/* Main content */}
{/* Disable animations for admin pages to prevent scroll jumps in Telegram Mini App */} {location.pathname.startsWith('/admin') ? ( children ) : ( {children} )}
{/* Mobile Bottom Navigation */}
); }