import { useEffect, useState } from 'react'; import { useLocation, Link } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { useAuthStore } from '@/store/auth'; import { useHaptic } from '@/platform'; import { useTelegramSDK } from '@/hooks/useTelegramSDK'; import { useHeaderHeight } from '@/hooks/useHeaderHeight'; import { useTheme } from '@/hooks/useTheme'; import { useBranding } from '@/hooks/useBranding'; import { useFeatureFlags } from '@/hooks/useFeatureFlags'; import { useScrollRestoration } from '@/hooks/useScrollRestoration'; import { themeColorsApi } from '@/api/themeColors'; import { isLogoPreloaded } from '@/api/branding'; import { cn } from '@/lib/utils'; import WebSocketNotifications from '@/components/WebSocketNotifications'; import CampaignBonusNotifier from '@/components/CampaignBonusNotifier'; import SuccessNotificationModal from '@/components/SuccessNotificationModal'; import { PromptDialogHost } from '@/components/PromptDialogHost'; import LanguageSwitcher from '@/components/LanguageSwitcher'; import TicketNotificationBell from '@/components/TicketNotificationBell'; import { SubscriptionIcon, GiftIcon, HomeIcon, CreditCardIcon, ChatIcon, UserIcon, UsersIcon, ShieldIcon, InfoIcon, LogoutIcon, SunIcon, MoonIcon, } from '@/components/icons'; import { MobileBottomNav } from './MobileBottomNav'; import { AppHeader } from './AppHeader'; import { BackgroundRenderer } from '@/components/backgrounds/BackgroundRenderer'; interface AppShellProps { children: React.ReactNode; } export function AppShell({ children }: AppShellProps) { const { t } = useTranslation(); const location = useLocation(); const isAdmin = useAuthStore((state) => state.isAdmin); const logout = useAuthStore((state) => state.logout); const { isFullscreen, safeAreaInset, contentSafeAreaInset, platform, isMobile } = useTelegramSDK(); const { mobile: headerHeight } = useHeaderHeight(); const haptic = useHaptic(); const { toggleTheme, isDark } = useTheme(); // Extracted hooks const { appName, logoLetter, hasCustomLogo, logoUrl } = useBranding(); const { referralEnabled, wheelEnabled, hasContests, hasPolls, giftEnabled } = useFeatureFlags(); useScrollRestoration(); // Theme toggle visibility const { data: enabledThemes } = useQuery({ queryKey: ['enabled-themes'], queryFn: themeColorsApi.getEnabledThemes, staleTime: 1000 * 60 * 5, }); const canToggleTheme = enabledThemes?.dark && enabledThemes?.light; // Only apply fullscreen UI adjustments on mobile Telegram (iOS/Android) const isMobileFullscreen = isFullscreen && isMobile; const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [isKeyboardOpen, setIsKeyboardOpen] = useState(false); // Reset keyboard state on route change — prevents bottom nav staying hidden after navigation useEffect(() => { setIsKeyboardOpen(false); }, [location.pathname]); // 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 — labels always visible (no hover-reveal gimmick) const desktopNav = [ { path: '/', label: t('nav.dashboard'), icon: HomeIcon }, { path: '/subscriptions', label: t('nav.subscription'), icon: SubscriptionIcon }, { path: '/balance', label: t('nav.balance'), icon: CreditCardIcon }, ...(referralEnabled ? [{ path: '/referral', label: t('nav.referral'), icon: UsersIcon }] : []), ...(giftEnabled ? [{ path: '/gift', label: t('nav.gift'), icon: GiftIcon }] : []), { path: '/support', label: t('nav.support'), icon: ChatIcon }, { path: '/info', label: t('nav.info'), icon: InfoIcon }, { 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'); }; // A single elegant nav link: icon + label always visible, with a shared // framer-motion pill that slides to the active item on navigation. const renderNavLink = ( path: string, label: string, Icon: React.ComponentType<{ className?: string }>, admin = false, ) => { const active = admin ? location.pathname.startsWith('/admin') : isActive(path); return ( {active && ( )} {label} ); }; // headerHeight comes from useHeaderHeight() — accounts for TG safe area in fullscreen return (
{/* Animated background renders via portal on document.body at z-index: -1 */} {/* 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} telegramPlatform={platform} wheelEnabled={wheelEnabled} referralEnabled={referralEnabled} hasContests={hasContests} hasPolls={hasPolls} giftEnabled={giftEnabled} /> {/* Desktop spacer */}
{/* Mobile spacer */}
{/* Main content */}
{children}
{/* Mobile Bottom Navigation */}
); }