// Reusable parts: Sidebar, Topbar, Stat, Quest row, Heatmap, ConfirmModal
// ── Local helpers ─────────────────────────────────────────────────────────────
const _toDate = (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 _recLabel = (rec) => {
if (!rec) return '';
if (rec === 'daily') return 'Todo dia';
if (rec === 'weekdays') return 'Seg – Sex';
if (rec === 'weekend') return 'Sab – Dom';
const map = { seg:'Seg', ter:'Ter', qua:'Qua', qui:'Qui', sex:'Sex', sab:'Sab', dom:'Dom' };
return rec.split(',').map(k => map[k] || k).join(', ');
};
// Build week grid entries for a recurring quest
const getWeekGrid = (recurrence, weekCompletions) => {
const KEYS = ['seg','ter','qua','qui','sex','sab','dom']; // index 0=Mon
const LABELS = ['Seg','Ter','Qua','Qui','Sex','Sab','Dom'];
// Monday of current week
const now = new Date();
const dayJS = now.getDay(); // 0=Sun
const diff = dayJS === 0 ? -6 : 1 - dayJS;
const mon = new Date(now);
mon.setDate(now.getDate() + diff);
mon.setHours(0, 0, 0, 0);
let indices;
if (recurrence === 'daily') indices = [0,1,2,3,4,5,6];
else if (recurrence === 'weekdays') indices = [0,1,2,3,4];
else if (recurrence === 'weekend') indices = [5,6];
else indices = recurrence.split(',').map(k => KEYS.indexOf(k)).filter(i => i >= 0);
const doneSet = new Set(weekCompletions || []);
const todayISO = _toDate(now);
const todayMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
return indices.map(i => {
const d = new Date(mon); d.setDate(mon.getDate() + i);
const dateStr = _toDate(d);
const dMs = d.getTime();
return {
date: dateStr,
label: LABELS[i],
done: doneSet.has(dateStr),
isToday: dateStr === todayISO,
isPast: dMs < todayMs,
isFuture: dMs > todayMs,
};
});
};
// ── Confirm Modal ─────────────────────────────────────────────────────────────
const ConfirmModal = ({ title, body, confirmLabel = 'Excluir', onConfirm, onCancel }) => (
e.stopPropagation()}>
{title}
{body}
);
// ── Sidebar ───────────────────────────────────────────────────────────────────
const Sidebar = ({ route, setRoute, unread, user, onLogout }) => {
const items = [
{ id: 'home', label: 'Modo Campanha', ico: 'shield' },
{ id: 'quests', label: 'Quest Log', ico: 'scroll', badge: unread },
{ id: 'badges', label: 'Salão de troféus', ico: 'trophy' },
{ id: 'rank', label: 'Ranking', ico: 'crown' },
{ id: 'profile', label: 'Aventureiro', ico: 'user' },
{ id: 'settings', label: 'Configurações', ico: 'cog' },
];
const initials = (user?.adventurerName || 'A').charAt(0).toUpperCase();
return (
);
};
// ── Topbar ────────────────────────────────────────────────────────────────────
const Topbar = ({ crumb, here, streak, onForge }) => (
Streak {streak} dias
);
// ── Stat card ─────────────────────────────────────────────────────────────────
const Stat = ({ kind, label, value, unit, sub, icoName }) => (
{label}
{React.createElement(I[icoName], {})}
{value}{unit && {unit}}
{sub &&
{sub}
}
);
// ── Quest row ─────────────────────────────────────────────────────────────────
const Quest = ({ q, onComplete, onCompleteDay, onDelete, compact }) => {
const [menuOpen, setMenuOpen] = React.useState(false);
const [confirmOpen, setConfirmOpen] = React.useState(false);
const cat = CATEGORIES.find(c => c.id === q.category) || CATEGORIES[0];
const diff = DIFFICULTIES.find(d => d.id === q.rarity);
const CatIco = I[cat.ico];
const handleDeleteConfirm = () => { setConfirmOpen(false); onDelete(q.id); };
React.useEffect(() => {
if (!menuOpen) return;
const close = () => setMenuOpen(false);
document.addEventListener('click', close);
return () => document.removeEventListener('click', close);
}, [menuOpen]);
// ── Recurring quest: build week grid ──
const weekDays = q.recurrence ? getWeekGrid(q.recurrence, q.weekCompletions) : null;
const allPastDone = weekDays && weekDays.filter(d => !d.isFuture).length > 0
&& weekDays.filter(d => !d.isFuture).every(d => d.done);
const isRegDone = !q.recurrence && q.status === 'done';
const questClass = ['quest', q.recurrence ? 'recurring' : '', isRegDone ? 'done' : ''].filter(Boolean).join(' ');
return (
<>
{/* Col 1: checkbox for one-time, ↻ badge for recurring */}
{q.recurrence ? (
↻
) : (
)}
{/* Col 2: meta + week grid */}
{q.title}
{!compact &&
{q.desc}
}
{diff.label}
{cat.label}
{q.recurrence && (
↻ {_recLabel(q.recurrence)}
)}
{!q.recurrence && q.deadline && q.deadline !== '—' && q.deadline !== 'Sem prazo' && (
{q.deadline}
)}
{/* Week grid */}
{weekDays && (
{weekDays.map(day => (
))}
{allPastDone && ✦ Semana completa!}
)}
{/* Col 3: reward */}
+{q.xp}
{q.recurrence ? 'XP / dia' : 'recompensa'}
{/* Col 4: menu */}
{confirmOpen && (
setConfirmOpen(false)}
/>
)}
>
);
};
// ── Heatmap ───────────────────────────────────────────────────────────────────
const Heatmap = ({ data }) => (
);
// ── Empty state ───────────────────────────────────────────────────────────────
const Empty = ({ ico = 'scroll', title, body }) => (
{React.createElement(I[ico], { sw: 1 })}
{title}
{body}
);
// ── Badge ─────────────────────────────────────────────────────────────────────
const Badge = ({ b, full, state }) => {
const unlocked = b.cond(state);
const p = Math.min(100, Math.round(b.progress(state) * 100));
return (
{b.glyph}
{b.name}
{b.req}
{unlocked ? 'Conquistada' : `${p}% concluído`}
{!full &&
}
);
};
window.ConfirmModal = ConfirmModal;
window.Sidebar = Sidebar;
window.Topbar = Topbar;
window.Stat = Stat;
window.Quest = Quest;
window.Heatmap = Heatmap;
window.Empty = Empty;
window.Badge = Badge;