/* global React */ const { useState, useEffect, useMemo } = React; // ──────────────────────────────────────────────────────────── // App — главный контейнер с роутингом между экранами // Phase 1 MVP: Лента / Карта / Статистика / Деталь события // (subs / settings / about — отложено в Phase 2) // ──────────────────────────────────────────────────────────── // Read initial screen from URL ?screen=... or Telegram start_param. function __initialTab() { try { const params = new URLSearchParams(window.location.search || ""); const fromUrl = params.get("screen"); if (fromUrl) return fromUrl; const tg = window.Telegram?.WebApp; const startParam = tg?.initDataUnsafe?.start_param; if (startParam) return startParam; } catch (e) {} return "home"; } // Theme override stored in localStorage. "auto" follows Telegram, "light"/ // "dark" force a fixed theme regardless of TG colorScheme. function __readThemePref() { try { return localStorage.getItem("eq.theme") || "auto"; } catch (e) { return "auto"; } } function __saveThemePref(v) { try { localStorage.setItem("eq.theme", v); } catch (e) {} } // Home variant pref — A (default, scientific) / B (map-first) / C (minimal) function __readHomeVariant() { try { return localStorage.getItem("eq.home-variant") || "A"; } catch (e) { return "A"; } } window.MiniApp = () => { const tg = window.TG; // Mini App auth gate: real Telegram clients (mobile AND Telegram Desktop) // expose a non-empty initData. A regular browser has none — there we must // NOT make authorized requests, NOT show global data as "yours", and NOT // surface a raw "missing tma authorization". Instead we render a clean gate // that sends the user to the bot. const inTelegram = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData); const [themePref, setThemePref] = useState(__readThemePref()); const [tgDark, setTgDark] = useState(tg.dark); const dark = themePref === "auto" ? tgDark : themePref === "dark"; const setDark = (next) => { // Manual toggle — store an explicit pref (light or dark) and exit auto. const v = next ? "dark" : "light"; setThemePref(v); __saveThemePref(v); }; // Onboarding gate: shown ONCE on first launch. Bypassed thereafter. const [needsOnboarding, setNeedsOnboarding] = useState( () => (window.__shouldShowOnboarding ? window.__shouldShowOnboarding() : false) ); const [homeVariant, setHomeVariantState] = useState(__readHomeVariant()); const setHomeVariant = (v) => { try { localStorage.setItem("eq.home-variant", v); } catch (e) {} setHomeVariantState(v); }; const [tab, setTab] = useState(__initialTab()); // home | map | stats | digest | settings | about const [detailEvent, setDetailEvent] = useState(null); // When the alarm view is active, user can press "Карта эпицентра" to fall // through to the regular EventDetailScreen even though mag>=6. const [forceDetail, setForceDetail] = useState(false); // Tsunami detail screen visibility (overlay over detail/alarm). const [tsunamiForEvent, setTsunamiForEvent] = useState(null); const T = dark ? window.TOKENS.dark : window.TOKENS.light; // Subscribe to Telegram theme changes — only relevant in "auto" mode. useEffect(() => { if (!tg.onThemeChange) return; return tg.onThemeChange((isDark) => setTgDark(isDark)); }, []); // Load data once on mount — only inside Telegram. Outside, we show the gate // and never touch Supabase/the backend. useEffect(() => { if (!inTelegram) return; window.DATA.reload(); }, []); // Pull user's mag_digest from backend on first mount, so the feed/digest // screens filter the same way as the Telegram digest. Silent on failure // (when opened outside Telegram, /status returns 401 and we just stay on // the default minimum magnitude — Mini App remains usable read-only). useEffect(() => { if (!inTelegram) return; if (!window.API || !window.DATA?.setUserMagFilter) return; window.API.getStatus() .then((s) => { if (s && typeof s.mag_digest === "number") { window.DATA.setUserMagFilter(s.mag_digest); } }) .catch(() => {}); }, []); // Reactive store const data = window.DATA.useDataState(); // Telegram BackButton: visible whenever we're not on the home tab or in detail useEffect(() => { if (!tg.available) return; const inSubScreen = !!detailEvent || tab !== "home"; if (inSubScreen) { tg.showBack(); const off = tg.onBack(() => { if (detailEvent) setDetailEvent(null); else setTab("home"); }); return () => { off(); tg.hideBack(); }; } else { tg.hideBack(); } }, [detailEvent, tab]); const openEvent = (ev) => { setDetailEvent(ev); setForceDetail(false); }; const closeDetail = () => { setDetailEvent(null); setForceDetail(false); }; // Auth gate — opened outside Telegram (no initData). Rendered AFTER all hooks // so hook order stays stable. No data has been fetched in this case. if (!inTelegram) { return ; } // Loading screen if (data.loading && data.events.length === 0) { return ; } // Hard error: show banner but try to render whatever we have if (data.error && data.events.length === 0) { return window.DATA.reload()} />; } // Onboarding takes over the whole screen on first launch if (needsOnboarding) { return ( { if (window.__markOnboardingSeen) window.__markOnboardingSeen(); setNeedsOnboarding(false); }} /> ); } // Tsunami detail overlay — shown over alarm/detail when user clicks the // tsunami warning. Fully covers the screen until back is pressed. if (tsunamiForEvent) { return ( setTsunamiForEvent(null)} /> ); } let screen; if (detailEvent) { // Strong events get the dramatic Alarm view; "Карта эпицентра" inside // it sets forceDetail=true so we fall through to the full Leaflet detail. if (detailEvent.mag >= 6.0 && !forceDetail) { screen = setForceDetail(true)} onShowTsunami={() => setTsunamiForEvent(detailEvent)} />; } else { screen = setTsunamiForEvent(detailEvent)} />; } } else if (tab === "safety") { screen = setTab("settings")} />; } else if (tab === "map") { screen = setTab("home")} />; } else if (tab === "stats") { screen = setTab("home")} />; } else if (tab === "digest") { screen = setTab("home")} onOpenEvent={openEvent} hours={12} />; } else if (tab === "settings") { screen = setTab("home")} onToggleTheme={() => setDark(!dark)} onOpenAbout={() => setTab("about")} onOpenSafety={() => setTab("safety")} homeVariant={homeVariant} onChangeHomeVariant={setHomeVariant} />; } else if (tab === "about") { screen = setTab("settings")} />; } else { // Home — variant switchable via Settings. Pass onOpenSettings so the // filter pill in Home A can jump straight to Settings. const goSettings = () => setTab("settings"); if (homeVariant === "B" && window.HomeScreenB) { screen = ; } else if (homeVariant === "C" && window.HomeScreenC) { screen = ; } else { screen = ; } } return (
{/* Top bar — Telegram WebApp style */}
{window.Icon.pulse(16)}
Сейсмомониторинг
{window.CONFIG.CHANNEL}
{!tg.available && ( )}
{/* Main content */}
{screen}
{/* Bottom navigation */} {!detailEvent && (
setTab("home")} icon={window.Icon.pulse(16)} label="Лента" /> setTab("digest")} icon={window.Icon.bell(16)} label="Сводка" /> setTab("map")} icon={window.Icon.map(16)} label="Карта" /> setTab("stats")} icon={window.Icon.chart(16)} label="Статистика" /> setTab("settings")} icon={window.Icon.settings(16)} label="Ещё" />
)}
); }; // Shown when the Mini App is opened outside Telegram (no initData). Clean // dead-end with a link to the bot — no data, no raw auth errors. const GateScreen = ({ T }) => (
{window.Icon.pulse(28)}
Сейсмомониторинг
Это мини-приложение открывается внутри Telegram. Запустите его через бота — так подгрузятся ваши настройки и персональная сводка.
Открыть @SeismicAlert_Russia_bot
); const LoadingScreen = ({ T }) => (
ЗАГРУЗКА ДАННЫХ
USGS · EMSC · Supabase
); const ErrorScreen = ({ T, error, onRetry }) => (
⚠️
Не удалось получить данные
{error}
); // Bootstrap // Babel-in-browser