/* 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 (
);
};
// 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. Запустите его через бота —
так подгрузятся ваши настройки и персональная сводка.