// app.jsx — router, global state, mount (load LAST)
const { useState: useStateApp, useRef: useRefApp, useEffect: useEffectApp } = React;
// Self-report quest player (hydration etc.)
function SelfReport({ quest, onDone, onClose }) {
return (
}/>
{quest.title}
완료했나요? 정직하게 체크하면 돼요.
마셨어요
나중에
);
}
function App() {
const [phase, setPhase] = useStateApp('ftue');
const initTheme = (() => { try { return localStorage.getItem('fb_theme') || 'light'; } catch (e) { return 'light'; } })();
const [theme, setTheme] = useStateApp(initTheme);
const [data, setData] = useStateApp({ level: 2, xp: 120, streak: 3, energy: 3, maxEnergy: 5, pro: false, nextSec: 1140, homeVariant: 'default', completedToday: false });
const [backend, setBackend] = useStateApp(null);
const [route, setRoute] = useStateApp({ name: 'home', params: {} });
const hist = useRefApp([]);
// apply theme synchronously on every render so it survives navigation
window.applyTheme(theme);
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
useEffectApp(() => {
let mounted = true;
const api = window.FitbodyBackend;
if (!api) return undefined;
api.bootstrap().then(snapshot => {
if (!mounted) return;
setBackend(snapshot);
setData(d => api.dataFromSnapshot ? api.dataFromSnapshot(snapshot, d) : d);
}).catch(() => {
if (!mounted) return;
const snapshot = api.snapshot();
setBackend(snapshot);
setData(d => api.dataFromSnapshot ? api.dataFromSnapshot(snapshot, d) : d);
});
return () => { mounted = false; };
}, []);
// energy recharge timer
useEffectApp(() => {
const iv = setInterval(() => {
setData(d => {
if (d.pro || d.energy >= d.maxEnergy) return d;
if (d.nextSec <= 1) return { ...d, energy: Math.min(d.maxEnergy, d.energy + 1), nextSec: 1500 };
return { ...d, nextSec: d.nextSec - 1 };
});
}, 1000);
return () => clearInterval(iv);
}, []);
const go = (name, params = {}) => { hist.current.push(route); setRoute({ name, params }); };
const back = () => { const h = hist.current; setRoute(h.length ? h.pop() : { name: 'home', params: {} }); };
const navTab = (tab) => { hist.current = []; setPhase('app'); setRoute({ name: tab, params: {} }); };
window.__nav = navTab;
// spend 1 energy for an extra quest; free===true bypasses (daily core quest)
const startQuest = (q, free) => {
if (free || data.pro || data.energy > 0) {
if (!free && !data.pro) setData(d => ({ ...d, energy: Math.max(0, d.energy - 1) }));
if (window.FitbodyBackend) window.FitbodyBackend.beginQuest(q).catch(() => {});
go('player', { quest: q });
} else {
go('energy', { quest: q });
}
};
const applyCompletion = (questOrXp) => {
const quest = typeof questOrXp === 'object' ? questOrXp : null;
const xp = quest ? (quest.xp || 10) : (questOrXp || 0);
if (quest && window.FitbodyBackend) {
window.FitbodyBackend.completeQuest(quest).catch(() => {});
}
setData(d => ({
...d,
xp: d.xp + xp,
energy: d.pro ? d.energy : Math.min(d.maxEnergy, d.energy + 1), // completing tops up 1
streak: d.completedToday ? d.streak : d.streak + 1,
completedToday: true,
homeVariant: 'completed',
}));
};
if (phase === 'ftue') {
return { setData(d => ({ ...d, completedToday: false, homeVariant: 'default' })); setPhase('app'); navTab('home'); }}/>;
}
const { name, params } = route;
if (name === 'player') {
const q = params.quest;
if (q.selfReport) return go('completion', { quest: q })}/>;
return go('completion', { quest: q })} onClose={back}/>;
}
if (name === 'completion') {
const q = params.quest || {};
const xp = q.xp || 10;
return { applyCompletion(q); navTab('home'); } }}
secondary={{ label: 'Progress 보기', onClick: () => { applyCompletion(q); navTab('progress'); } }}
/>;
}
if (name === 'energy') return ;
if (name === 'ad') return { setData(d => ({ ...d, energy: Math.min(d.maxEnergy, d.energy + 1) })); back(); }}/>;
if (name === 'subscribe') return {
if (window.FitbodyBackend) window.FitbodyBackend.subscribe('pro').then(url => { if (url) window.location.href = url; }).catch(() => {});
setData(d => ({ ...d, pro: true }));
back();
}} back={back}/>;
if (name === 'academy') return navTab('home')} startQuest={startQuest} data={data} spendEnergy={() => setData(d => ({ ...d, energy: d.pro ? d.energy : Math.max(0, d.energy - 1) }))} onReward={(xp) => setData(d => ({ ...d, xp: d.xp + (xp || 0) }))}/>;
if (name === 'academyAt') return navTab('home')} startQuest={startQuest} data={data} spendEnergy={() => setData(d => ({ ...d, energy: d.pro ? d.energy : Math.max(0, d.energy - 1) }))} onReward={(xp) => setData(d => ({ ...d, xp: d.xp + (xp || 0) }))}/>;
if (name === 'bodycheck') return startQuest(q)}/>;
if (name === 'flow') return ;
if (name === 'quest') return ;
if (name === 'league') return ;
if (name === 'invite') return ;
if (name === 'progress') return