// Global state — fully API-driven // ── Fetch helper ────────────────────────────────────────────────────────────── const api = (method, url, body) => fetch(url, { method, credentials: 'include', headers: body ? { 'Content-Type': 'application/json' } : {}, body: body ? JSON.stringify(body) : undefined, }).then(async (res) => { const data = await res.json(); if (!data.ok) throw new Error(data.error || 'Erro desconhecido'); return data; }); // ── Date helpers (shared with parts.jsx via window) ────────────────────────── const toDateStr = (d) => { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; }; const todayStr = () => toDateStr(new Date()); // Is a recurring quest due today and not yet completed? const isRecurringDueToday = (q) => { if (!q.recurrence) return false; const today = todayStr(); const done = new Set(q.weekCompletions || []); if (done.has(today)) return false; const DAY_KEYS = ['dom','seg','ter','qua','qui','sex','sab']; // 0=Sun..6=Sat const todayKey = DAY_KEYS[new Date().getDay()]; if (q.recurrence === 'daily') return true; if (q.recurrence === 'weekdays') return ['seg','ter','qua','qui','sex'].includes(todayKey); if (q.recurrence === 'weekend') return ['sab','dom'].includes(todayKey); return q.recurrence.split(',').includes(todayKey); }; window.toDateStr = toDateStr; window.todayStr = todayStr; window.isRecurringDueToday = isRecurringDueToday; // ── Main hook ───────────────────────────────────────────────────────────────── const useGameState = () => { const [user, setUser] = React.useState(null); const [authLoading, setAuthLoading] = React.useState(true); const [quests, setQuests] = React.useState([]); const [history, setHistory] = React.useState([]); const [heatData, setHeatData] = React.useState(Array(140).fill(0)); const [ranking, setRanking] = React.useState([]); const [route, setRoute] = React.useState('home'); const [showForge, setShowForge] = React.useState(false); const [celeb, setCeleb] = React.useState(null); const [toasts, setToasts] = React.useState([]); React.useEffect(() => { api('GET', 'api/auth/me.php') .then((data) => { setUser(data.user); return loadAll(); }) .catch(() => {}) .finally(() => setAuthLoading(false)); }, []); const loadAll = async () => { const [qRes, hRes, heatRes, rankRes] = await Promise.all([ api('GET', 'api/quests/list.php'), api('GET', 'api/history/list.php'), api('GET', 'api/activity/heatmap.php'), api('GET', 'api/ranking/list.php'), ]); setQuests(qRes.quests); setHistory(hRes.history); setHeatData(heatRes.heat); setRanking(rankRes.ranking); }; // ── Auth ──────────────────────────────────────────────────────────────────── const login = async (email, password) => { const data = await api('POST', 'api/auth/login.php', { email, password }); setUser(data.user); document.documentElement.setAttribute('data-theme', data.user.theme || 'dark'); await loadAll(); }; const register = async (name, email, password) => { const data = await api('POST', 'api/auth/register.php', { name, email, password }); setUser(data.user); document.documentElement.setAttribute('data-theme', data.user.theme || 'dark'); await loadAll(); }; const logout = async () => { await api('POST', 'api/auth/logout.php'); setUser(null); setQuests([]); setHistory([]); setHeatData(Array(140).fill(0)); setRanking([]); setRoute('home'); }; const updateUser = async (updates) => { const data = await api('POST', 'api/user/update.php', updates); setUser(data.user); if (updates.theme) document.documentElement.setAttribute('data-theme', updates.theme); }; // ── Toasts ────────────────────────────────────────────────────────────────── const pushToast = (toast) => { const id = Date.now() + Math.random(); setToasts((t) => [...t, { ...toast, id }]); setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2900); }; // ── Quest: complete one-time ───────────────────────────────────────────────── const completeQuest = async (id) => { const q = quests.find((x) => x.id === id); if (!q || q.status === 'done' || q.recurrence) return; const oldLevel = user?.lvl?.level ?? 1; setQuests((qs) => qs.map((x) => x.id === id ? { ...x, status: 'done', completedAt: Date.now() } : x)); try { const data = await api('POST', 'api/quests/complete.php', { id }); setUser((u) => ({ ...u, ...data.stats })); pushToast({ kind: 'xp', title: 'Missão concluída!', xp: q.xp }); if (data.stats.lvl.level > oldLevel) setTimeout(() => setCeleb({ newLevel: data.stats.lvl.level }), 350); const [hRes, heatRes, rankRes] = await Promise.all([ api('GET', 'api/history/list.php'), api('GET', 'api/activity/heatmap.php'), api('GET', 'api/ranking/list.php'), ]); setHistory(hRes.history); setHeatData(heatRes.heat); setRanking(rankRes.ranking); } catch (err) { setQuests((qs) => qs.map((x) => x.id === id ? { ...x, status: 'open', completedAt: null } : x)); pushToast({ kind: 'error', title: 'Erro', message: err.message }); } }; // ── Quest: complete a specific day of a recurring quest ────────────────────── const completeDayRecurring = async (questId, date) => { const q = quests.find((x) => x.id === questId); if (!q || !q.recurrence) return; const oldLevel = user?.lvl?.level ?? 1; // Optimistic: add date to weekCompletions setQuests((qs) => qs.map((x) => x.id === questId ? { ...x, weekCompletions: [...(x.weekCompletions || []), date] } : x )); try { const data = await api('POST', 'api/quests/complete_day.php', { id: questId, date }); setUser((u) => ({ ...u, ...data.stats })); pushToast({ kind: 'xp', title: 'Dia concluído!', xp: q.xp }); if (data.stats.lvl.level > oldLevel) setTimeout(() => setCeleb({ newLevel: data.stats.lvl.level }), 350); const [hRes, heatRes] = await Promise.all([ api('GET', 'api/history/list.php'), api('GET', 'api/activity/heatmap.php'), ]); setHistory(hRes.history); setHeatData(heatRes.heat); } catch (err) { // Revert setQuests((qs) => qs.map((x) => x.id === questId ? { ...x, weekCompletions: (x.weekCompletions || []).filter((d) => d !== date) } : x )); pushToast({ kind: 'error', title: 'Erro', message: err.message }); } }; // ── Quest: forge & delete ──────────────────────────────────────────────────── const addQuest = async (questData) => { try { const data = await api('POST', 'api/quests/create.php', questData); setQuests((qs) => [data.quest, ...qs]); pushToast({ kind: 'forge', title: 'Missão forjada!', xp: data.quest.xp }); const hRes = await api('GET', 'api/history/list.php'); setHistory(hRes.history); } catch (err) { pushToast({ kind: 'error', title: 'Erro ao forjar', message: err.message }); } }; const deleteQuest = async (id) => { setQuests((qs) => qs.filter((x) => x.id !== id)); try { await api('POST', 'api/quests/delete.php', { id }); } catch (err) { const qRes = await api('GET', 'api/quests/list.php'); setQuests(qRes.quests); pushToast({ kind: 'error', title: 'Erro ao excluir', message: err.message }); } }; // ── Derived ────────────────────────────────────────────────────────────────── const totalXp = user?.totalXp ?? 0; const lvl = user?.lvl ?? { level: 1, intoLevel: 0, needLevel: 500 }; const tier = user?.tier ?? { name: 'Iniciante', idx: 0 }; const streak = user?.streak ?? 0; const bestStreak = user?.bestStreak ?? 0; const totalCompleted = quests.filter((q) => q.status === 'done').length; const completedByRarity = quests .filter((q) => q.status === 'done') .reduce((acc, q) => { acc[q.rarity] = (acc[q.rarity] || 0) + 1; return acc; }, {}); const stateForBadges = { xp: totalXp, streak, totalCompleted, completedByRarity }; const unlockedBadges = BADGES.filter((b) => b.cond(stateForBadges)); const dismissCeleb = () => setCeleb(null); return { user, authLoading, login, register, logout, updateUser, quests, history, heatData, ranking, streak, bestStreak, totalXp, lvl, tier, totalCompleted, unlockedBadges, stateForBadges, route, setRoute, showForge, setShowForge, celeb, dismissCeleb, toasts, completeQuest, completeDayRecurring, addQuest, deleteQuest, }; }; window.useGameState = useGameState; window.api = api;