// app.jsx — production bootstrap: тема + загрузка живых данных из /api/portfolio. // Заменяет инлайн-скрипт дизайн-исходника. Подключается последним (после // portfolio-data / portfolio-atoms / portfolio-screen). const { useState, useEffect } = React; const DARK = "taupe"; // вариант B — тёмная const LIGHT = "exchangeLight"; // вариант B — светлая // Telegram WebApp SDK const tg = window.Telegram && window.Telegram.WebApp; const INIT_DATA = (tg && tg.initData) || ""; // Тема берётся из Telegram / системы устройства (без сохранённого выбора — // приложение всегда следует текущей теме окружения). function detectEnv() { try { if (tg && tg.colorScheme) return tg.colorScheme === "light" ? "light" : "dark"; } catch (e) {} if (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches) return "light"; return "dark"; } // ---- theme toggle ---- function SunIcon() { return ( ); } function MoonIcon() { return ( ); } function ThemeToggle({ mode, onToggle }) { const dark = mode === "dark"; return ( ); } // ---- status screens (loading / error / empty) ---- function CenterMsg({ theme, children }) { const vars = window.THEMES[theme].vars; return (
{children}
); } function Spinner() { return ( ); } // ---- main app ---- function App() { const [mode, setMode] = useState(detectEnv); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const theme = mode === "dark" ? DARK : LIGHT; const bg = window.THEMES[theme].vars["--bg"]; // фон страницы + theme-color под текущую тему useEffect(() => { document.body.style.background = bg; const page = document.getElementById("page"); if (page) page.style.background = bg; const meta = document.querySelector('meta[name="theme-color"]') || (() => { const m = document.createElement("meta"); m.name = "theme-color"; document.head.appendChild(m); return m; })(); meta.content = bg; }, [mode, bg]); // живое следование теме устройства/Telegram: реагируем на смену темы useEffect(() => { const apply = () => setMode(detectEnv()); let tgBound = false; try { if (tg && tg.onEvent) { tg.onEvent("themeChanged", apply); tgBound = true; } } catch (e) {} let mq = null; if (window.matchMedia) { mq = window.matchMedia("(prefers-color-scheme: dark)"); if (mq.addEventListener) mq.addEventListener("change", apply); else if (mq.addListener) mq.addListener(apply); } return () => { try { if (tgBound && tg.offEvent) tg.offEvent("themeChanged", apply); } catch (e) {} if (mq) { if (mq.removeEventListener) mq.removeEventListener("change", apply); else if (mq.removeListener) mq.removeListener(apply); } }; }, []); async function load() { if (!INIT_DATA) { setError("Откройте приложение через кнопку в Telegram."); setLoading(false); return; } try { const res = await fetch("api/portfolio", { headers: { Authorization: "tma " + INIT_DATA }, cache: "no-store", }); if (res.status === 401) { setError("Сессия истекла. Закройте и снова откройте мини-апп."); setLoading(false); return; } if (res.status === 403) { setError("Доступ запрещён."); setLoading(false); return; } if (!res.ok) { setError("Ошибка сервера (" + res.status + "). Попробуйте обновить."); setLoading(false); return; } const json = await res.json(); setData(window.mapPortfolio(json)); setError(null); setLoading(false); } catch (e) { // при наличии старых данных не затираем экран — просто показываем последнюю ошибку if (!data) setError("Нет соединения. Потяните «Обновить»."); setLoading(false); } } useEffect(() => { if (tg) { try { tg.ready(); tg.expand(); } catch (e) {} } load(); // eslint-disable-next-line }, []); const toggle = () => setMode((m) => (m === "dark" ? "light" : "dark")); // первичная загрузка if (loading && !data) return
Загрузка портфеля…
; // ошибка без данных if (error && !data) return (
{error}
); // пустой портфель if (data && data.empty) return Портфель пуст.
Добавьте сделки через бота — и они появятся здесь.
; return ( } />); } ReactDOM.createRoot(document.getElementById("root")).render();