import { Link, useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useState, useEffect, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../../store/auth' import LanguageSwitcher from '../LanguageSwitcher' import PromoDiscountBadge from '../PromoDiscountBadge' import TicketNotificationBell from '../TicketNotificationBell' import { contestsApi } from '../../api/contests' import { pollsApi } from '../../api/polls' import { brandingApi } from '../../api/branding' import { wheelApi } from '../../api/wheel' import { themeColorsApi } from '../../api/themeColors' import { useTheme } from '../../hooks/useTheme' // Fallback branding from environment variables const FALLBACK_NAME = import.meta.env.VITE_APP_NAME || 'Cabinet' const FALLBACK_LOGO = import.meta.env.VITE_APP_LOGO || 'V' interface LayoutProps { children: React.ReactNode } // Icons as simple SVG components const HomeIcon = () => ( ) const SubscriptionIcon = () => ( ) const WalletIcon = () => ( ) const UsersIcon = () => ( ) const ChatIcon = () => ( ) const UserIcon = () => ( ) const LogoutIcon = () => ( ) // Theme toggle icons const SunIcon = () => ( ) const MoonIcon = () => ( ) const MenuIcon = () => ( ) const CloseIcon = () => ( ) const GamepadIcon = () => ( ) const ClipboardIcon = () => ( ) const InfoIcon = () => ( ) const CogIcon = () => ( ) const WheelIcon = () => ( ) export default function Layout({ children }: LayoutProps) { const { t } = useTranslation() const location = useLocation() const { user, logout, isAdmin, isAuthenticated } = useAuthStore() const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const { toggleTheme, isDark } = useTheme() const [userPhotoUrl, setUserPhotoUrl] = useState(null) // Fetch enabled themes from API - same source of truth as AdminSettings const { data: enabledThemes } = useQuery({ queryKey: ['enabled-themes'], queryFn: themeColorsApi.getEnabledThemes, staleTime: 1000 * 60 * 5, // 5 minutes }) // Only show theme toggle if both themes are enabled const canToggle = enabledThemes?.dark && enabledThemes?.light // Get user photo from Telegram WebApp useEffect(() => { try { const tg = (window as any).Telegram?.WebApp const photoUrl = tg?.initDataUnsafe?.user?.photo_url if (photoUrl) { setUserPhotoUrl(photoUrl) } } catch (e) { console.warn('Failed to get Telegram user photo:', e) } }, []) // Lock body scroll and scroll to top when mobile menu is open useEffect(() => { if (mobileMenuOpen) { // Scroll to top so header is visible window.scrollTo({ top: 0, behavior: 'instant' }) document.body.style.overflow = 'hidden' } else { document.body.style.overflow = '' } return () => { document.body.style.overflow = '' } }, [mobileMenuOpen]) // Fetch branding settings const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: brandingApi.getBranding, staleTime: 60000, // 1 minute retry: 1, }) // Computed branding values - use fallback only if branding not loaded yet const appName = branding ? branding.name : FALLBACK_NAME // Empty string is valid (logo-only mode) 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' // Fallback title if name is empty }, [appName]) // Fetch contests and polls counts to determine if they should be shown const { data: contestsCount } = useQuery({ queryKey: ['contests-count'], queryFn: contestsApi.getCount, enabled: isAuthenticated, staleTime: 60000, // 1 minute retry: false, }) const { data: pollsCount } = useQuery({ queryKey: ['polls-count'], queryFn: pollsApi.getCount, enabled: isAuthenticated, staleTime: 60000, // 1 minute retry: false, }) // Fetch wheel config to check if enabled const { data: wheelConfig } = useQuery({ queryKey: ['wheel-config'], queryFn: wheelApi.getConfig, enabled: isAuthenticated, staleTime: 60000, // 1 minute retry: false, }) const navItems = useMemo(() => { const items = [ { path: '/', label: t('nav.dashboard'), icon: HomeIcon }, { path: '/subscription', label: t('nav.subscription'), icon: SubscriptionIcon }, { path: '/balance', label: t('nav.balance'), icon: WalletIcon }, { path: '/referral', label: t('nav.referral'), icon: UsersIcon }, { path: '/support', label: t('nav.support'), icon: ChatIcon }, ] // Only show contests if there are available contests if (contestsCount && contestsCount.count > 0) { items.push({ path: '/contests', label: t('nav.contests'), icon: GamepadIcon }) } // Only show polls if there are available polls if (pollsCount && pollsCount.count > 0) { items.push({ path: '/polls', label: t('nav.polls'), icon: ClipboardIcon }) } items.push({ path: '/info', label: t('nav.info'), icon: InfoIcon }) return items }, [t, contestsCount, pollsCount]) // Separate navItems for desktop that includes wheel (if enabled) const desktopNavItems = useMemo(() => { const items = [...navItems] // Add wheel before info if enabled if (wheelConfig?.is_enabled) { const infoIndex = items.findIndex(item => item.path === '/info') if (infoIndex !== -1) { items.splice(infoIndex, 0, { path: '/wheel', label: t('nav.wheel'), icon: WheelIcon }) } else { items.push({ path: '/wheel', label: t('nav.wheel'), icon: WheelIcon }) } } return items }, [navItems, wheelConfig, t]) const adminNavItems = [ { path: '/admin', label: t('admin.nav.title'), icon: CogIcon }, ] const isActive = (path: string) => location.pathname === path const isAdminActive = () => location.pathname.startsWith('/admin') return (
{/* Header */}
{/* Logo */}
{hasCustomLogo && logoUrl ? ( {appName ) : ( {logoLetter} )}
{appName && ( {appName} )} {/* Desktop Navigation */} {/* Right side */}
{/* Theme toggle button - only show if both themes are enabled */} {canToggle && ( )} {/* Profile - Desktop */}
{user?.first_name || user?.username || `#${user?.telegram_id}`}
{/* Mobile menu button */}
{/* Mobile menu - fixed overlay */} {mobileMenuOpen && (
{/* Backdrop */}
setMobileMenuOpen(false)} /> {/* Menu content */}
{/* User info */}
{userPhotoUrl ? ( Avatar { e.currentTarget.style.display = 'none' e.currentTarget.nextElementSibling?.classList.remove('hidden') }} /> ) : null}
{user?.first_name || user?.username}
@{user?.username || `ID: ${user?.telegram_id}`}
{/* Nav items */}
)} {/* Main Content */}
{children}
{/* Mobile Bottom Navigation - only core items */}
) }