/* ═══════════════════════════════════════════════════════════ Dating Coach — Telegram Mini App Single-file build для Babel-in-browser. Все экраны и логика тут, чтобы не зависеть от порядка подгрузки внешних JSX-скриптов. ═══════════════════════════════════════════════════════════ */ const { useState, useEffect, useCallback, useMemo, useRef } = React; const TG = window.Telegram && window.Telegram.WebApp; /* ───────── Icon set (1.7px stroke, 24 viewBox) ───────── */ function Icon({ name, size = 18, style }) { const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", style, }; const P = { home: <>, book: <>, trophy: <>, chart: <>, user: <>, flame: <>, bolt: <>, play: <>, arrow: <>, back: <>, check: <>, heart: <>, sparkle: <>, mic: <>, eye: <>, lock: <>, msg: <>, users: <>, cal: <>, bell: <>, settings: <>, target: <>, clock: <>, crown: <>, chev: <>, moon: <>, smile: <>, lang: <>, logout: <>, trend: <>, medal: <>, gift: <>, fire: <>, info: <>, palette: <>, shield: <>, }; return {P[name] || P.sparkle}; } /* ───────── API layer ───────── */ let _initData = ""; function authHeaders() { const h = { "Content-Type": "application/json" }; if (_initData) h["X-Telegram-Init-Data"] = _initData; return h; } async function apiGet(url) { const r = await fetch(url, { credentials: "include", headers: authHeaders() }); if (!r.ok) { const e = new Error(r.status); e.status = r.status; throw e; } return r.json(); } async function apiPost(url, body) { const r = await fetch(url, { method: "POST", credentials: "include", headers: authHeaders(), body: body ? JSON.stringify(body) : undefined, }); if (!r.ok) { const e = new Error(r.status); e.status = r.status; throw e; } return r.json(); } async function apiPatch(url, body) { const r = await fetch(url, { method: "PATCH", credentials: "include", headers: authHeaders(), body: body ? JSON.stringify(body) : undefined, }); if (!r.ok) { const e = new Error(r.status); e.status = r.status; throw e; } return r.json(); } /* ───────── helpers ───────── */ const LEVEL_META = { beginner: { name: "Начальный", short: "Начальный", idx: "I", sub: "Фундамент уверенности" }, intermediate: { name: "Средний", short: "Средний", idx: "II", sub: "Техники и отношения" }, advanced: { name: "Продвинутый", short: "Продвинутый", idx: "III", sub: "Мастерство и рост" }, }; const LEVEL_ORDER = ["beginner", "intermediate", "advanced"]; const POINTS_TO_NEXT = { beginner: 450, intermediate: 600, advanced: 0 }; function levelMeta(id) { return LEVEL_META[id] || { name: id, short: id, idx: "?", sub: "" }; } function makeInitials(profile) { if (!profile) return "?"; const fn = (profile.first_name || "").trim(); const ln = (profile.last_name || "").trim(); if (fn || ln) return ((fn[0] || "") + (ln[0] || "")).toUpperCase() || "?"; const un = (profile.username || "").trim(); return un ? un[0].toUpperCase() : "?"; } function displayName(profile) { if (!profile) return "Студент"; return profile.first_name || profile.username || "Студент"; } function greeting() { const h = new Date().getHours(); if (h < 6) return "Доброй ночи"; if (h < 12) return "Доброе утро"; if (h < 18) return "Добрый день"; return "Добрый вечер"; } function haptic(kind = "light") { try { if (TG && TG.HapticFeedback) { if (kind === "success") TG.HapticFeedback.notificationOccurred("success"); else TG.HapticFeedback.impactOccurred(kind); } } catch {} } /* Группировка ачивок по префиксу id (бэкенд хранит без category) */ function groupAchievements(list) { const groups = { lessons: { name: "Уроки", items: [] }, streak: { name: "Серии", items: [] }, points: { name: "Баллы", items: [] }, level: { name: "Уровни", items: [] }, special: { name: "Особые", items: [] }, }; list.forEach(a => { if (a.id.startsWith("lessons_") || a.id === "first_lesson") groups.lessons.items.push(a); else if (a.id.startsWith("streak_")) groups.streak.items.push(a); else if (a.id.startsWith("points_")) groups.points.items.push(a); else if (a.id.startsWith("level_")) groups.level.items.push(a); else groups.special.items.push(a); }); return Object.values(groups).filter(g => g.items.length > 0); } /* первый незавершённый урок текущего уровня */ function findCurrentLesson(structure, profileLevel) { if (!structure) return null; const levelOrderId = profileLevel || "beginner"; // приоритет — начать с текущего уровня, потом проверить разблокированные предыдущие const prefer = [levelOrderId, ...LEVEL_ORDER.filter(x => x !== levelOrderId)]; for (const lvId of prefer) { const lv = structure.find(l => l.id === lvId); if (!lv || !lv.unlocked) continue; let idx = 0; for (const m of lv.modules) { for (const les of m.lessons) { idx++; if (!les.completed) { return { level: lv, module: m, lesson: les, lessonIndex: idx }; } } } } return null; } /* ───────── Toast ───────── */ const ToastCtx = React.createContext(() => {}); function ToastProvider({ children }) { const [msg, setMsg] = useState(null); const tRef = useRef(null); const show = useCallback((m) => { setMsg(m); clearTimeout(tRef.current); tRef.current = setTimeout(() => setMsg(null), 2800); }, []); return ( {children} {msg &&
{msg}
}
); } const useToast = () => React.useContext(ToastCtx); /* ───────── App-wide data store (минимальный) ───────── */ const DataCtx = React.createContext(null); function DataProvider({ profile, setProfile, children }) { const [progress, setProgress] = useState(null); const [structure, setStructure] = useState(null); const [structureLoading, setStructureLoading] = useState(false); const loadProgress = useCallback(async () => { try { setProgress(await apiGet("/api/users/me/progress")); } catch {} }, []); const loadStructure = useCallback(async (force = false) => { if (structure && !force) return structure; if (structureLoading) return null; setStructureLoading(true); try { const s = await apiGet("/api/lessons/structure"); setStructure(s); return s; } catch { return null; } finally { setStructureLoading(false); } }, [structure, structureLoading]); const invalidate = useCallback(() => { setStructure(null); loadProgress(); }, [loadProgress]); return ( {children} ); } const useData = () => React.useContext(DataCtx); /* ───────── Router (стек экранов) ───────── */ const NavCtx = React.createContext(null); function NavProvider({ children, initial = { id: "home" } }) { const [stack, setStack] = useState([initial]); const top = stack[stack.length - 1]; const push = useCallback((screen) => { setStack(s => [...s, screen]); }, []); const replace = useCallback((screen) => { setStack([screen]); }, []); const back = useCallback(() => { setStack(s => s.length > 1 ? s.slice(0, -1) : s); }, []); const goTab = useCallback((tabId) => { setStack([{ id: tabId }]); }, []); // Telegram BackButton useEffect(() => { if (!TG || !TG.BackButton) return; if (stack.length > 1) { TG.BackButton.show(); const onClick = () => back(); TG.BackButton.onClick(onClick); return () => { try { TG.BackButton.offClick(onClick); } catch {} }; } else { TG.BackButton.hide(); } }, [stack.length, back]); return ( {children} ); } const useNav = () => React.useContext(NavCtx); /* ───────── Reusable: TabBar ───────── */ function TabBar({ active }) { const nav = useNav(); const tabs = [ { id: "home", icon: "home", label: "Главная" }, { id: "learn", icon: "book", label: "Уроки" }, { id: "ach", icon: "trophy", label: "Награды" }, { id: "stats", icon: "chart", label: "Статистика" }, ]; return (
{tabs.map(t => (
{ haptic("light"); nav.goTab(t.id); }}> {t.label}
))}
); } /* ═══════════════════════════════════════════════════════════ Экран 1: Auth ═══════════════════════════════════════════════════════════ */ function AuthScreen({ onAuthed }) { const [phase, setPhase] = useState("loading"); // loading | needLogin const toast = useToast(); useEffect(() => { let cancelled = false; (async () => { // 1) JWT cookie try { const s = await apiGet("/api/auth/session"); if (s && s.authenticated && !cancelled) { onAuthed(s.user); return; } } catch {} // 2) Portal cookie try { const p = await apiGet("/api/auth/platform/auto"); if (p && p.ok && !cancelled) { onAuthed(p.user); return; } } catch {} // 3) Telegram WebApp initData if (TG && TG.initData) { try { const t = await apiPost("/api/auth/telegram", { initData: TG.initData }); if (t && t.ok && !cancelled) { onAuthed(t.user); return; } } catch {} } if (!cancelled) setPhase("needLogin"); })(); return () => { cancelled = true; }; }, [onAuthed]); return (
TG Play · Dating Coach

Коуч по общению и уверенности

36 уроков, реальные сценарии и ежедневная практика.

3 уровня — от основ до продвинутых техник общения
17 достижений и прогресс, который видно
Ежедневная серия — 5 минут в день достаточно
{phase === "loading" ? ( ) : ( Войти через tgplay.ru )}

{phase === "loading" ? "Подгружаем твой профиль…" : "Авторизуйся на tgplay.ru и вернись сюда"}

); } /* ═══════════════════════════════════════════════════════════ Экран 2: Home / Dashboard ═══════════════════════════════════════════════════════════ */ function HomeScreen() { const { profile, progress, structure, loadStructure } = useData(); const nav = useNav(); useEffect(() => { loadStructure(); }, [loadStructure]); const initials = makeInitials(profile); const name = displayName(profile); const streak = progress?.current_streak ?? 0; const completed = progress?.completed_lessons ?? 0; const total = progress?.total_lessons ?? 36; const points = progress?.points ?? profile?.points ?? 0; // Неделя: восстановим последние 7 дней по стрику. // Локальный понедельник как старт недели. const today = new Date(); const dow = (today.getDay() + 6) % 7; // 0=Mon...6=Sun const weekLabels = ["Пн","Вт","Ср","Чт","Пт","Сб","Вс"]; const week = weekLabels.map((d, i) => { if (i < dow) { // прошедший день недели — закрашен, если стрик «дотягивается» назад const daysBack = dow - i; return { d, done: streak >= (daysBack + 1) }; } if (i === dow) return { d, today: true, done: streak >= 1 }; return { d }; }); const cur = useMemo(() => findCurrentLesson(structure, profile?.level), [structure, profile]); // continue card — индекс урока «X из 36» const lessonNo = cur ? (() => { let n = 0; for (const lv of structure || []) { for (const m of lv.modules) { for (const l of m.lessons) { n++; if (lv.id === cur.level.id && m.id === cur.module.id && l.id === cur.lesson.id) return n; } } } return n; })() : 0; return (
{initials}

{greeting()}

{name}
{/* streak hero */}
Серия
{streak} {streak === 1 ? "день" : streak >= 2 && streak <= 4 ? "дня подряд" : "дней подряд"}

{streak === 0 ? "Начни сегодня — открой первый урок" : streak < 3 ? "Хорошее начало, не теряй темп" : streak < 7 ? "Уже три дня! Продолжай" : "Дисциплина — это сила"}

{week.map((x, i) => (
{x.done ? : x.today ? : null}
{x.d}
))}
{/* continue lesson */}

{cur ? "Продолжить" : "Все уроки пройдены"}

nav.goTab("learn")}>Все уроки
{cur ? (
nav.push({ id: "lesson", levelId: cur.level.id, moduleId: cur.module.id, lessonId: cur.lesson.id, })}>
Урок {String(lessonNo).padStart(2,"0")} из {total}
{cur.lesson.title}
{cur.module.name.replace(/^\W+\s*/, "")} · {levelMeta(cur.level.id).short}
) : (
nav.goTab("ach")}>
Курс пройден
Посмотри свои достижения
{progress?.achievements_count || 0}/17 наград
)} {/* фокус — реальные числа */}

Твой прогресс

{points}
Очков
+10 за урок
{total ? Math.round(completed/total*100) : 0}%
Курс пройден
{completed} из {total}
{progress?.achievements_count || 0}
Достижений
из 17
); } /* ═══════════════════════════════════════════════════════════ Экран 3: Learning — список уровней + модули ═══════════════════════════════════════════════════════════ */ function LearningScreen() { const { profile, progress, structure, loadStructure } = useData(); const nav = useNav(); const [openLevel, setOpenLevel] = useState(profile?.level || "beginner"); useEffect(() => { loadStructure(); }, [loadStructure]); if (!structure) { return (
Обучение
); } const currentLevel = structure.find(l => l.id === (profile?.level || "beginner")) || structure[0]; const curStats = (() => { let total = 0, done = 0; currentLevel.modules.forEach(m => m.lessons.forEach(l => { total++; if (l.completed) done++; })); return { total, done, pct: total ? Math.round(done/total*100) : 0 }; })(); const points = progress?.points ?? profile?.points ?? 0; const meta = levelMeta(currentLevel.id); const opened = structure.find(l => l.id === openLevel); return (
Обучение
Твой текущий уровень

{meta.short} — {meta.sub.toLowerCase()}

Ты освоил {curStats.done} {curStats.done === 1 ? "урок" : "уроков"} из {curStats.total}. {curStats.total - curStats.done > 0 && ` Осталось ${curStats.total - curStats.done}, чтобы открыть следующий.`}

{curStats.done}/{curStats.total}
Уроков
{points}
Очков
{progress?.current_streak || 0}д
Серия
{structure.map(lv => { let total = 0, done = 0; lv.modules.forEach(m => m.lessons.forEach(l => { total++; if (l.completed) done++; })); const pct = total ? Math.round(done/total*100) : 0; const state = !lv.unlocked ? "locked" : (lv.id === (profile?.level || "beginner") ? "current" : "done"); const lm = levelMeta(lv.id); return (
lv.unlocked && setOpenLevel(lv.id)}>
{state === "locked" ? : lm.idx}
{lm.name} уровень
{lm.sub}
{done}/{total}
); })}

Модули — {levelMeta(opened.id).short}

{(() => { let t=0, d=0; opened.modules.forEach(m => m.lessons.forEach(l => { t++; if (l.completed) d++; })); return {d}/{t}; })()}
{!opened.unlocked && (
🔒 Этот уровень ещё закрыт. Пройди уроки текущего уровня.
)}
{opened.modules.map((m, mi) => { const total = m.lessons.length; const done = m.lessons.filter(l => l.completed).length; const state = done === total ? "done" : (done > 0 ? "current" : "todo"); return (
{String(mi+1).padStart(2,"0")}
{m.name}
{done}/{total}
{m.lessons.map((les, li) => { const prevCompleted = li === 0 ? true : m.lessons[li-1].completed; const lstate = les.completed ? "done" : (!opened.unlocked ? "lock" : prevCompleted ? "current" : "todo"); return (
opened.unlocked && nav.push({ id: "lesson", levelId: opened.id, moduleId: m.id, lessonId: les.id, })}>
{lstate === "done" ? : lstate === "current" ? : lstate === "lock" ? : null}
{les.title}
); })}
); })}
); } /* ═══════════════════════════════════════════════════════════ Экран 4: Lesson reader ═══════════════════════════════════════════════════════════ */ function LessonScreen({ levelId, moduleId, lessonId }) { const { structure, loadStructure, invalidate } = useData(); const nav = useNav(); const toast = useToast(); const [data, setData] = useState(null); const [error, setError] = useState(null); const [completing, setCompleting] = useState(false); useEffect(() => { loadStructure(); }, [loadStructure]); useEffect(() => { let cancelled = false; setData(null); setError(null); (async () => { try { const d = await apiGet(`/api/lessons/${levelId}/${moduleId}/${lessonId}`); if (!cancelled) setData(d); } catch (e) { if (!cancelled) setError(e.status === 403 ? "locked" : "error"); } })(); return () => { cancelled = true; }; }, [levelId, moduleId, lessonId]); // Хлебные крошки и индекс const crumbs = (() => { if (!structure) return null; const lv = structure.find(l => l.id === levelId); if (!lv) return null; const m = lv.modules.find(x => x.id === moduleId); if (!m) return null; const lessonIdx = m.lessons.findIndex(l => l.id === lessonId); return { lv, m, lessonIdx, lvMeta: levelMeta(lv.id) }; })(); // Глобальный номер урока для прогресс-бара чтения const globalIdx = useMemo(() => { if (!structure) return null; let n = 0, total = 0; let cur = 0; for (const lv of structure) { for (const m of lv.modules) { for (const l of m.lessons) { n++; total++; if (lv.id === levelId && m.id === moduleId && l.id === lessonId) cur = n; } } } return { cur, total }; }, [structure, levelId, moduleId, lessonId]); const onComplete = async () => { if (completing) return; setCompleting(true); try { const r = await apiPost(`/api/lessons/${levelId}/${moduleId}/${lessonId}/complete`); haptic("success"); invalidate(); // celebration nav.replace({ id: "celeb", result: r, levelId, moduleId, lessonId, title: data?.title || "", }); } catch { toast("Не удалось завершить урок"); } finally { setCompleting(false); } }; return (
{globalIdx ? `Урок ${String(globalIdx.cur).padStart(2,"0")}` : "Урок"}
{globalIdx && (
)} {error === "locked" ? (
🔒 Этот уровень ещё заблокирован.
Пройди все уроки текущего уровня.
) : error ? (
Ошибка загрузки урока
) : !data ? (
) : ( <>
{crumbs && (
{crumbs.lvMeta.short} · {crumbs.m.name.replace(/^\W+\s*/, "")}
)}

{data.title}

+{data.points} очков {data.completed && ( Пройден )}
{data.completed ? ( ) : ( )}
)}
); } /* ═══════════════════════════════════════════════════════════ Экран 5: Celebration ═══════════════════════════════════════════════════════════ */ function CelebrationScreen({ result, levelId, moduleId, lessonId, title }) { const { structure, profile, progress } = useData(); const nav = useNav(); // Найти следующий урок const next = useMemo(() => { if (!structure) return null; for (const lv of structure) { for (const m of lv.modules) { for (let i = 0; i < m.lessons.length; i++) { const l = m.lessons[i]; if (lv.id === levelId && m.id === moduleId && l.id === lessonId) { // следующий — может быть в этом же модуле if (i + 1 < m.lessons.length) { return { level: lv, module: m, lesson: m.lessons[i+1] }; } // или первый незавершённый дальше по структуре return findCurrentLesson(structure, profile?.level); } } } } return null; }, [structure, profile, levelId, moduleId, lessonId]); // % прохождения текущего уровня const pct = useMemo(() => { if (!structure) return 0; const lvId = result?.level_up || profile?.level || "beginner"; const lv = structure.find(l => l.id === lvId); if (!lv) return 0; let t = 0, d = 0; lv.modules.forEach(m => m.lessons.forEach(l => { t++; if (l.completed) d++; })); return t ? Math.round(d/t*100) : 0; }, [structure, profile, result]); const newAch = result?.new_achievements?.[0]; const wasAlready = result?.already_completed; return (
{result?.level_up ? "Новый уровень!" : wasAlready ? "Урок уже пройден" : "Урок завершён"}

{result?.level_up ? `Открыт ${levelMeta(result.level_up).name.toLowerCase()} уровень` : "Ещё один шаг к уверенности"}

{wasAlready ? `Урок «${title}» был пройден ранее.` : `Ты прошёл урок «${title}». Молодец — не останавливайся.`}

+{wasAlready ? 0 : 10}
Очков
{result?.current_streak ?? progress?.current_streak ?? 0}
Серия
{pct}%
Уровень
{newAch && (
{newAch.emoji || "🏆"}
Новое достижение
{newAch.name}
)}
{next ? ( ) : ( )}
); } /* ═══════════════════════════════════════════════════════════ Экран 6: Achievements ═══════════════════════════════════════════════════════════ */ const ACH_ICON_MAP = { first_lesson:"target", lessons_5:"book", lessons_10:"medal", lessons_20:"crown", lessons_all:"trophy", streak_3:"fire", streak_7:"flame", streak_14:"bolt", streak_30:"shield", level_intermediate:"bolt", level_advanced:"crown", points_50:"bolt", points_100:"sparkle", points_200:"gift", points_500:"crown", early_bird:"sparkle", completionist:"crown", }; function AchievementsScreen() { const [list, setList] = useState(null); const [error, setError] = useState(false); useEffect(() => { let cancelled = false; (async () => { try { const data = await apiGet("/api/achievements/"); if (!cancelled) setList(data); } catch { if (!cancelled) setError(true); } })(); return () => { cancelled = true; }; }, []); if (error) { return (
Достижения
Не удалось загрузить достижения
); } if (!list) { return (
Достижения
); } const total = list.length; const earned = list.filter(a => a.unlocked).length; const groups = groupAchievements(list); const r = 30, c = 2 * Math.PI * r; const pct = total ? earned / total : 0; const renderAch = (a) => (
{a.unlocked ? : }
{a.name}
); return (
Достижения
{earned}/{total}
{earned === total ? "Все достижения собраны!" : earned === 0 ? "Начни путь — пройди первый урок" : "На пути к перфекционисту"}
{earned === total ? "Невероятно — ты собрал всё" : `Осталось ${total - earned} достижений до полного набора`}
{groups.map(g => { const gEarned = g.items.filter(a => a.unlocked).length; return (
{g.name} {gEarned}/{g.items.length}
{g.items.map(renderAch)}
); })}
); } /* ═══════════════════════════════════════════════════════════ Экран 7: Stats ═══════════════════════════════════════════════════════════ */ function StatsScreen() { const { profile, progress, structure, loadStructure } = useData(); useEffect(() => { loadStructure(); }, [loadStructure]); const initials = makeInitials(profile); const name = displayName(profile); const lvm = levelMeta(profile?.level); const points = progress?.points ?? profile?.points ?? 0; const completed = progress?.completed_lessons ?? 0; const total = progress?.total_lessons ?? 36; const streak = progress?.current_streak ?? 0; const best = progress?.best_streak ?? 0; const ach = progress?.achievements_count ?? 0; const curLevel = structure?.find(l => l.id === (profile?.level || "beginner")); const curStats = (() => { if (!curLevel) return { done: 0, total: 0, pct: 0 }; let t = 0, d = 0; curLevel.modules.forEach(m => m.lessons.forEach(l => { t++; if (l.completed) d++; })); return { done: d, total: t, pct: t ? Math.round(d/t*100) : 0 }; })(); const toNext = POINTS_TO_NEXT[profile?.level || "beginner"]; const remaining = toNext > 0 ? Math.max(0, toNext - points) : 0; // Heatmap не выводим — нет API daily activity. Вместо неё — разбивка по модулям текущего уровня. return (
Статистика
{initials}
{name}
{lvm.name.toUpperCase()} · {curStats.done}/{curStats.total} УРОКОВ
{points} очков
{toNext > 0 && (
до {profile?.level === "beginner" ? "среднего" : "продвинутого"}: {remaining}
)}
Серия
{streak} дн.
▲ лучшая {best} дн.
Уроки
{completed}/{total}
{total ? Math.round(completed/total*100) : 0}% курса
Достижения
{ach}/17
{Math.round(ach/17*100)}% открыто
Очки
{points}
+10 за урок
{curLevel && (
Разбивка — {lvm.short} уровень
{curLevel.modules.map(m => { const t = m.lessons.length; const d = m.lessons.filter(l => l.completed).length; const pct = t ? Math.round(d/t*100) : 0; return (
{m.name.replace(/^\W+\s*/, "")}
{d}/{t}
); })}
)}
); } /* ═══════════════════════════════════════════════════════════ Экран 8: Settings ═══════════════════════════════════════════════════════════ */ function SettingsScreen() { const { profile, setProfile } = useData(); const nav = useNav(); const toast = useToast(); const [s, setS] = useState(null); const [saving, setSaving] = useState(false); useEffect(() => { let cancelled = false; (async () => { try { const data = await apiGet("/api/settings/"); if (!cancelled) setS(data); } catch {} })(); return () => { cancelled = true; }; }, []); const Switch = ({ on, onClick, disabled }) => (
Настройки
{initials}
{name}
tgplay.ru · {lvm.name} уровень
{!s ? (
) : ( <>
Уведомления
Уведомления
Прогресс и достижения
update({ notifications: !s.notifications })}/>
Ежедневное напоминание
Не дать серии прерваться
update({ daily_reminder: !s.daily_reminder })}/>
Время напоминания
update({ reminder_time: e.target.value })}/>
Информация
Часовой пояс
{s.timezone || "—"}
О приложении
v 1.1
)}
Выйти
); } /* ═══════════════════════════════════════════════════════════ App root + router ═══════════════════════════════════════════════════════════ */ function Router() { const nav = useNav(); const t = nav.top; switch (t.id) { case "home": return ; case "learn": return ; case "ach": return ; case "stats": return ; case "lesson": return ; case "celeb": return ; case "settings": return ; default: return ; } } function App() { const [profile, setProfile] = useState(null); const [bootDone, setBootDone] = useState(false); // первичный bootstrap — не блокируем, просто отдаём AuthScreen и он сам решает useEffect(() => { setBootDone(true); }, []); // когда логинимся/разлогиниваемся — обновляем шапку tgplay useEffect(() => { if (window.TgPlayHeader && profile) { try { window.TgPlayHeader.setUser(profile); } catch {} } }, [profile]); if (!bootDone) return null; if (!profile) { return ( { setProfile(u); haptic("success"); }}/> ); } return ( ); } /* отдельный компонент чтобы тянуть прогресс при первом открытии любого экрана */ function BootstrapData() { const { loadProgress } = useData(); useEffect(() => { loadProgress(); }, [loadProgress]); return null; } /* ───────── Init ───────── */ (function init() { if (TG) { try { TG.ready(); TG.expand(); if (TG.initData) _initData = TG.initData; } catch {} } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(); })();