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