// 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 === 'login'
? <>Novo por aqui? switchMode('register')}>Criar uma conta >
: <>Já tem conta? switchMode('login')}>Entrar >
}
);
};
// ── 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.
s.setShowForge(true)}> Forjar nova missão
s.setRoute('quests')}> Abrir Quest Log
{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
setTab('open')}> Abertas {open.length}
setTab('done')}> Concluídas {done.length}
{list.length === 0
?
: list.map((q) => )
}
Crônicas recentes
s.setRoute('quests')}>Ver tudo
{s.history.length === 0
?
: (
{s.history.slice(0, 5).map((h) => (
{h.kind === 'level' ? : h.kind === 'forge' ? : }
{h.when}
))}
)
}
Troféus recentes
s.setRoute('badges')}>Salão
{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.
setFilter('all')}>Todas {s.quests.length}
setFilter('open')}>Abertas {s.quests.filter(q => q.status === 'open').length}
setFilter('done')}>Concluídas {doneCount}
setCat('all')}> Todas categorias
{CATEGORIES.map((c) => {
const Ico = I[c.ico];
return setCat(c.id)}> {c.label} ;
})}
{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 (
);
})}
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 (
);
})}
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
Tema
setTheme('dark')}>
Caverna
Modo escuro
setTheme('light')}>
Pergaminho
Modo claro
>
);
};
window.AuthScreen = AuthScreen;
window.Home = Home;
window.QuestsScreen = QuestsScreen;
window.BadgesScreen = BadgesScreen;
window.RankScreen = RankScreen;
window.ProfileScreen = ProfileScreen;
window.SettingsScreen = SettingsScreen;