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 AnimatedBackground from '../AnimatedBackground'
import { contestsApi } from '../../api/contests'
import { pollsApi } from '../../api/polls'
import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo } from '../../api/branding'
import { wheelApi } from '../../api/wheel'
import { themeColorsApi } from '../../api/themeColors'
import { promoApi } from '../../api/promo'
import { useTheme } from '../../hooks/useTheme'
import { useTelegramWebApp } from '../../hooks/useTelegramWebApp'
// 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 = () => (
)
const FullscreenIcon = () => (
)
const ExitFullscreenIcon = () => (
)
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)
const { isTelegramWebApp, isFullscreen, isFullscreenSupported, toggleFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp()
// 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])
// State to track if logo image has loaded
const [logoLoaded, setLogoLoaded] = useState(false)
// Fetch branding settings with localStorage cache for instant load
const { data: branding } = useQuery({
queryKey: ['branding'],
queryFn: async () => {
const data = await brandingApi.getBranding()
setCachedBranding(data) // Update cache
// Preload logo in background
preloadLogo(data)
return data
},
initialData: getCachedBranding() ?? undefined, // Use cached data immediately
staleTime: 60000, // 1 minute
refetchOnWindowFocus: true,
retry: 1,
})
// Computed branding values - use fallback only if no branding and no cache
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,
})
// Fetch active discount to determine mobile layout
const { data: activeDiscount } = useQuery({
queryKey: ['active-discount'],
queryFn: promoApi.getActiveDiscount,
enabled: isAuthenticated,
staleTime: 30000,
})
// Check if promo is active (to hide language switcher on mobile)
const isPromoActive = activeDiscount?.is_active && activeDiscount?.discount_percent
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 (
{/* Animated Background */}
{/* Header */}
{/* Logo */}
{/* Always show letter as fallback */}
{logoLetter}
{/* Logo image with smooth fade-in */}
{hasCustomLogo && logoUrl && (
setLogoLoaded(true)}
/>
)}
{appName && (
{appName}
)}
{/* Desktop Navigation */}
{/* Right side */}
{/* Fullscreen toggle - only show in Telegram WebApp */}
{isTelegramWebApp && isFullscreenSupported && (
)}
{/* Theme toggle button - only show if both themes are enabled */}
{canToggle && (
)}
{/* Hide language switcher on mobile when promo is active */}