// Screen components: AuthScreen, Home, Quests, Badges, Rank, Profile, Settings // ── Auth ────────────────────────────────────────────────────────────────────── const AuthScreen = ({ login, register }) => { const [mode, setMode] = React.useState('login'); const [name, setName] = React.useState(''); const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); const switchMode = (m) => { setMode(m); setError(''); }; const submit = async (e) => { e.preventDefault(); setError(''); setLoading(true); try { if (mode === 'login') { await login(email, password); } else { await register(name.trim() || 'Aventureiro', email, password); } } catch (err) { setError(err.message); } finally { setLoading(false); } }; return (
XP
Produtividade Gameficada
v2.0 · modo campanha

{mode === 'login' ? 'Entrar na campanha' : 'Iniciar a jornada'}

{mode === 'login' ? 'Bem-vindo de volta, aventureiro.' : 'Crie sua conta e comece a acumular XP.'}

{mode === 'register' && (
setName(e.target.value)} autoFocus />
)}
setEmail(e.target.value)} autoFocus={mode === 'login'} required />
setPassword(e.target.value)} required />
{error && (
{error}
)}
{mode === 'login' ? <>Novo por aqui? : <>Já tem conta? }
); }; // ── Home ────────────────────────────────────────────────────────────────────── const Home = ({ s }) => { // Recurring quests are always 'open'; exclude them from the "Concluídas" tab const open = s.quests.filter((q) => q.status === 'open'); const done = s.quests.filter((q) => q.status === 'done' && !q.recurrence); const [tab, setTab] = React.useState('open'); const list = tab === 'open' ? open : done; const xpPct = Math.round((s.lvl.intoLevel / s.lvl.needLevel) * 100); // Count missions due today: one-time with deadline='Hoje' + recurring not yet done today const today = open.filter((q) => q.recurrence ? isRecurringDueToday(q) : q.deadline === 'Hoje').length; const tier = s.tier; const initials = (s.user?.adventurerName || 'A').charAt(0).toUpperCase(); return ( <> s.setShowForge(true)} />
Painel da campanha · {new Date().toLocaleDateString('pt-BR', { weekday: 'long', day: '2-digit', month: 'long' })}

O dia te aguarda, aventureiro.

Você tem {today} missões marcadas para hoje e {open.length - today} na fila. Cada execução vira XP, cada dia consistente acende a chama do streak.

{initials}

{s.user?.adventurerName || 'Aventureiro'}

{tier.name} · Caminho do executor
{s.lvl.level} Nível atual {s.lvl.intoLevel} / {s.lvl.needLevel} XP
{TIERS.map((t, i) => (
{t.name}
))}
+{done.reduce((x, q) => x + q.xp, 0)} nesta sessão} /> {xpPct}% rumo ao próximo} /> Melhor sequência: {s.bestStreak}} /> {today} com prazo hoje} />

Fila de execução

{list.length === 0 ? : list.map((q) => ) }

Crônicas recentes

{s.history.length === 0 ? : (
{s.history.slice(0, 5).map((h) => (
{h.kind === 'level' ? : h.kind === 'forge' ? : }
{h.when}
))}
) }

Troféus recentes

{BADGES.slice(0, 4).map((b) => )}

Mapa da consistência

últimas 20 semanas
); }; // ── Quest Log ───────────────────────────────────────────────────────────────── const QuestsScreen = ({ s }) => { const [filter, setFilter] = React.useState('all'); const [cat, setCat] = React.useState('all'); // "done" filter excludes recurring quests (they're ongoing) const matchesFilter = (q) => { if (filter === 'done') return q.status === 'done' && !q.recurrence; if (filter === 'open') return q.status === 'open'; return true; // 'all' }; const filtered = s.quests.filter((q) => matchesFilter(q) && (cat === 'all' || q.category === cat)); const doneCount = s.quests.filter((q) => q.status === 'done' && !q.recurrence).length; return ( <> s.setShowForge(true)} />
Quest log · {filtered.length} miss{filtered.length === 1 ? 'ão' : 'ões'}

Toda missão em campo

Filtre por estado e categoria. Conclua para ganhar XP — missões raras valem o esforço.

{CATEGORIES.map((c) => { const Ico = I[c.ico]; return ; })}
{filtered.length === 0 ? : filtered.map((q) => ) }
); }; // ── Badges ──────────────────────────────────────────────────────────────────── const BadgesScreen = ({ s }) => ( <> s.setShowForge(true)} />
Hall of fame · {s.unlockedBadges.length} de {BADGES.length} conquistados

Salão de troféus

Marcos que você gravou em pedra. Cada um conta uma decisão repetida.

{BADGES.map((b) => )}
); // ── Ranking ─────────────────────────────────────────────────────────────────── const RankScreen = ({ s }) => { const list = s.ranking.length > 0 ? s.ranking : [{ name: s.user?.adventurerName || 'Você', title: s.tier.name, xp: s.totalXp, you: true }]; const myPos = list.findIndex((r) => r.you) + 1; return ( <> s.setShowForge(true)} />
Guilda · classificação geral

Ranking pessoal

Você está em #{myPos} de {list.length} aventureiros.

{list.map((r, i) => { const cls = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : ''; return (
{i === 0 ? : `#${i + 1}`}
{r.name.charAt(0).toUpperCase()}
{r.name}{r.you && ' (você)'}{r.title}
{r.xp.toLocaleString('pt-BR')}XP
); })}
); }; // ── Profile ─────────────────────────────────────────────────────────────────── const ProfileScreen = ({ s }) => { const name = s.user?.adventurerName || 'Aventureiro'; const initials = name.charAt(0).toUpperCase(); const attrs = [ { ico: 'fist', label: 'Foco', sub: 'Missões épicas concluídas', v: s.stateForBadges.completedByRarity?.epic || 0 }, { ico: 'feather', label: 'Constância', sub: 'Streak atual', v: s.streak + 'd' }, { ico: 'eye', label: 'Visão', sub: 'Missões totais concluídas', v: s.totalCompleted }, { ico: 'compass', label: 'Disciplina', sub: 'Melhor sequência', v: s.bestStreak + 'd' }, { ico: 'spark', label: 'Aura', sub: 'Troféus conquistados', v: s.unlockedBadges.length }, ]; return ( <> s.setShowForge(true)} />
Ficha do aventureiro

{name}

Sua identidade nesta campanha — atributos que crescem com sua prática.

{initials}

{name}

{s.tier.name} · Nível {s.lvl.level}
{attrs.map((a) => { const Ico = I[a.ico]; return (
{a.label}{a.sub}
{a.v}
); })}

Distribuição de XP por categoria

{CATEGORIES.map((c) => { const Ico = I[c.ico]; const earned = s.quests.filter((q) => q.status === 'done' && q.category === c.id).reduce((sum, q) => sum + q.xp, 0); const allMax = Math.max(...CATEGORIES.map((cat) => s.quests.filter((q) => q.status === 'done' && q.category === cat.id).reduce((sum, q) => sum + q.xp, 0)), 200); const p = Math.min(100, Math.round((earned / allMax) * 100)); return (
{c.label} {earned} XP
); })}

Próximos marcos

{BADGES.filter((b) => !b.cond(s.stateForBadges)).slice(0, 4).map((b) => )}
); }; // ── Settings ────────────────────────────────────────────────────────────────── const SettingsScreen = ({ s, theme, setTheme }) => { const [name, setName] = React.useState(s.user?.adventurerName || ''); const [email, setEmail] = React.useState(s.user?.email || ''); const [pw, setPw] = React.useState(''); const [saving, setSaving] = React.useState(false); const [saved, setSaved] = React.useState(false); const [saveErr, setSaveErr] = React.useState(''); const save = async () => { setSaving(true); setSaveErr(''); try { const updates = { adventurerName: name, email }; if (pw.trim()) updates.password = pw; await s.updateUser(updates); setSaved(true); setPw(''); setTimeout(() => setSaved(false), 2500); } catch (err) { setSaveErr(err.message); } finally { setSaving(false); } }; return ( <> s.setShowForge(true)} />
Preferências

Configurações

Ajuste o tom e seus dados de campanha.

Aparência

Conta

setName(e.target.value)} />
setEmail(e.target.value)} />
setPw(e.target.value)} />
{saveErr &&
{saveErr}
}
); }; window.AuthScreen = AuthScreen; window.Home = Home; window.QuestsScreen = QuestsScreen; window.BadgesScreen = BadgesScreen; window.RankScreen = RankScreen; window.ProfileScreen = ProfileScreen; window.SettingsScreen = SettingsScreen;