// ============================================
// 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 (
);
}
// ============================================
// "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(
);