// ============================================ // MariusVPN — App shell, navigation, entry // ============================================ const { useState, useEffect, useCallback } = React; const { api, Icon, FMT, MOCK, Logo, Card, Button, IconButton, Badge, Spinner, ToastHost, useToast, LoginScreen, DashboardScreen, SubscriptionScreen, PaymentsScreen, DevicesScreen, TrafficScreen, ReferralScreen, HelpScreen, } = window; const NAV = [ { id: 'dashboard', label: 'Главная', icon: 'home', shortLabel: 'Главная' }, { id: 'subscription', label: 'Подписка', icon: 'crown', shortLabel: 'Подписка' }, { id: 'devices', label: 'Устройства', icon: 'device', shortLabel: 'Устройства' }, { id: 'traffic', label: 'Трафик', icon: 'chart', shortLabel: 'Трафик' }, { id: 'payments', label: 'Оплата', icon: 'card', shortLabel: 'Оплата' }, { id: 'referral', label: 'Рефералы', icon: 'gift', shortLabel: 'Рефералы' }, { id: 'help', label: 'Помощь', icon: 'help', shortLabel: 'Помощь' }, ]; // Mobile bottom-nav: 4 primary + "more" const MOBILE_NAV = ['dashboard', 'devices', 'traffic', 'payments']; // ============================================ // SIDEBAR (desktop) // ============================================ function Sidebar({ route, onRoute, me, onLogout }) { return ( ); } // ============================================ // BOTTOM NAV (mobile) // ============================================ function BottomNav({ route, onRoute, onMore }) { return ( ); } // ============================================ // MOBILE TOP BAR // ============================================ function MobileHeader({ me, onMore }) { return (
{me && ( )}
); } // ============================================ // "MORE" SHEET — mobile-only side links // ============================================ function MoreSheet({ open, onClose, onRoute, me, onLogout }) { if (!open) return null; const items = NAV.filter(n => !MOBILE_NAV.includes(n.id)); return (
{me && (
{me.user.initials}
{me.user.name}
{me.user.telegram}
)}
{items.map(item => ( ))}
); } // ============================================ // APP // ============================================ function App() { const [authed, setAuthed] = useState(MOCK); // MOCK: auto-authed const [booted, setBooted] = useState(MOCK); // auth check finished? const [route, setRoute] = useState('dashboard'); const [me, setMe] = useState(null); const [moreOpen, setMoreOpen] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const toast = useToast(); // Auth bootstrap: WebApp initData > ?token= OTP > existing session cookie. useEffect(() => { if (MOCK) return; (async () => { const url = new URL(window.location.href); const token = url.searchParams.get('token'); try { const tgInitData = window.Telegram?.WebApp?.initData; if (tgInitData) { const r = await fetch('/api/tg/cabinet-auth', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ init_data: tgInitData }), }); if (!r.ok) throw new Error('initData auth failed'); setAuthed(true); } else if (token) { await api('/login', { method: 'POST', body: { token } }); url.searchParams.delete('token'); window.history.replaceState({}, '', url.pathname + url.search); setAuthed(true); } else { await api('/whoami'); setAuthed(true); } } catch (e) { setAuthed(false); } finally { setBooted(true); } })(); }, []); // Load /me on auth const loadMe = useCallback(() => api('/me').then(setMe).catch(() => {}), []); useEffect(() => { if (authed) loadMe(); }, [authed, refreshKey, loadMe]); // Scroll to top on route change useEffect(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }, [route]); if (!booted) { return
; } if (!authed) { return { setAuthed(true); setBooted(true); }} />; } const onRoute = (r) => { setRoute(r); setMoreOpen(false); }; const onRefresh = () => setRefreshKey(k => k + 1); const logout = async () => { try { if (!MOCK) await api('/logout', { method: 'POST' }); } catch (e) {} setAuthed(false); toast.info('Вы вышли из аккаунта'); }; const ScreenComponent = ({ dashboard: , subscription: , devices: , traffic: , payments: , referral: , help: , })[route] || ; return (
setMoreOpen(true)} />
{ScreenComponent}
setMoreOpen(true)} /> setMoreOpen(false)} onRoute={onRoute} me={me} onLogout={logout} />
); } // ============================================ // MOUNT // ============================================ const root = ReactDOM.createRoot(document.getElementById('root')); root.render( );