// app.jsx — root component (sidebar + main + cmd+k + tweaks)
const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React;
/**
* PageWrapper — сохраняет состояние страницы между переключениями вкладок.
*
* Раньше main-контейнер использовал `
` + условный рендер,
* из-за чего React при каждом переключении полностью unmount-ил предыдущую
* страницу. Терялись: выбранный чат, скролл, фильтры, набираемый текст,
* in-flight запросы (без AbortController оставались висеть и писать в
* размонтированный setState → React warnings). Это был главный root cause
* ощущения «забывает где я был / нажал и ничего не сохранилось».
*
* Сейчас:
* - Lazy mount: страница монтируется при первом visible=true. Если юзер
* никогда не заходил на «Чеки» — компонент не запускается совсем.
* - Keep alive: после первого монтирования компонент **остаётся** в DOM,
* скрытый через display:none. Его state, polling-таймеры, in-flight
* fetch и т.п. — продолжают жить. Юзер возвращается — всё на месте.
*
* Trade-off: polling всех ранее открытых страниц продолжает работать в фоне.
* Для большинства случаев это нормально (запросы лёгкие). Если станет тяжело
* — можно подключить Page Visibility API и паузить polling когда invisible.
*/
function PageWrapper({ visible, children }) {
const [mounted, setMounted] = useStateA(visible);
useEffectA(() => {
if (visible && !mounted) setMounted(true);
}, [visible, mounted]);
if (!mounted) return null;
return
{children}
;
}
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"homeVariant": "B",
"density": "regular",
"accent": "indigo",
"sidebar": "full"
}/*EDITMODE-END*/;
const ACCENT_MAP = {
indigo: "#6366f1",
emerald: "#10b981",
rose: "#f43f5e",
amber: "#f59e0b",
fuchsia: "#d946ef",
};
// Логотип сайдбара: реальный Telegram-аватар БОТА (/api/owners/bot/avatar).
// Пока фото грузится или если его нет (404/ошибка) — прежний зелёный градиент
// (тот же контейнер w-7 h-7 rounded-lg, размер/скругление не меняются).
function BrandMark() {
const [imgFailed, setImgFailed] = useStateA(false);
return (
);
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [active, setActive] = useStateA("home");
// Мобильное меню: на узких экранах (телефон) сайдбар прячется и выезжает
// по кнопке-бургеру, чтобы контент занимал всю ширину, а не уезжал за край.
const [mobileNavOpen, setMobileNavOpen] = useStateA(false);
const [cmdkOpen, setCmdkOpen] = useStateA(false);
const [hint, setHint] = useStateA(null);
const [statusPills, setStatusPills] = useStateA(STATUS_PILLS);
const gTimer = useRefA(null);
const allItems = NAV.flatMap((g) => g.items);
const current = allItems.find((n) => n.id === active);
/* Polling статуса бота: первый запрос сразу + потом каждые 10 сек.
Берёт реальные данные из /api/status. */
useEffectA(() => {
let stopped = false;
const refresh = async () => {
const pills = await fetchStatus();
if (!stopped) setStatusPills(pills);
};
refresh();
const id = setInterval(refresh, 10_000);
return () => { stopped = true; clearInterval(id); };
}, []);
// Density CSS var (could be expanded — for now just applied to spacing inline)
const densityScale = t.density === "compact" ? 0.85 : t.density === "spacious" ? 1.15 : 1;
// [бежевый редизайн] Хоткеи (⌘K + g-leader переключение разделов) убраны по просьбе.
// Sidebar collapse
const sidebarCollapsed = t.sidebar === "icons";
const accent = ACCENT_MAP[t.accent] || ACCENT_MAP.indigo;
return (
{/* Затемнение под выехавшим меню — только на узких экранах, клик закрывает. */}
{mobileNavOpen && (
setMobileNavOpen(false)} />
)}
{/* Кнопка-бургер — только на узких экранах, открывает боковое меню. */}
Zxckotik
{statusPills.map((s) => (
{s.label}:{s.value}
))}
{/* Каждая страница в PageWrapper'е → keep-alive между переключениями.
При первом visit ленится монтироваться, дальше живёт скрытая.
Это убирает «забывает где я был» — главный source ощущения,
что админка ненадёжная. */}