// CourseMap.jsx — ruta del curso con bloqueo/desbloqueo. window.CourseMap const { useState: useStateM } = React; function notaStr(score, total) { if (score === null || score === undefined) return '—'; return ((Math.round((score / total) * 100) / 10).toFixed(1)).replace('.', ','); } function StateDot({ kind }) { // kind: done | open | lock const map = { done: { bg: 'var(--success)', ch: '✓', col: '#fff' }, open: { bg: 'var(--terracotta)', ch: '', col: '#fff' }, lock: { bg: 'var(--bg-deep)', ch: '🔒', col: 'var(--ink-3)' }, }; const s = map[kind] || map.lock; return {s.ch}; } function CourseMap({ onOpenProgress, user }) { const S = window.CourseStore; const units = S.units(); const [open, setOpen] = useStateM(() => { // abrir la primera unidad no completada y desbloqueada const u = units.find(x => S.unitUnlocked(x.slug) && !S.unitCompleted(x.slug)); return u ? u.slug : (units[0] && units[0].slug); }); const [quiz, setQuiz] = useStateM(null); const [expl, setExpl] = useStateM(null); const [, setTick] = useStateM(0); const refresh = () => setTick(t => t + 1); const launchPartial = (unit, part) => setQuiz({ slug: unit.slug, partKey: part.n, bank: part.bank, n: S.PARTIAL.N, min: S.PARTIAL.MIN, title: part.title, subtitle: unit.ud + ' · Parte ' + part.n }); const launchFinal = (unit) => setQuiz({ slug: unit.slug, partKey: 'final', bank: unit.finalBank, n: S.FINAL.N, min: S.FINAL.MIN, title: 'Cuestionario final', subtitle: unit.ud + ' · ' + unit.topic }); const done = S.unitsCompletedCount(); const pct = Math.round((done / units.length) * 100); return (
{/* encabezado de progreso */}
Curso · Matemáticas 4º ESO

Hola, {user.display || user.name}

{done} / {units.length} unidades
{/* ruta de unidades */}
{units.map((unit, idx) => { const unlocked = S.unitUnlocked(unit.slug); const completed = S.unitCompleted(unit.slug); const isOpen = open === unit.slug; const pr = S.getProgress()[unit.slug]; return (
{/* cabecera de unidad */} {/* cuerpo expandido */} {isOpen && unlocked && (
{unit.parts.map(part => { const pUn = S.partUnlocked(unit.slug, part.n); const pPassed = pr.parts[part.n].passed; return (
Parte {part.n} · {part.title}
{pPassed ?
Superada · mejor nota {notaStr(pr.parts[part.n].best, S.PARTIAL.N)} · {pr.parts[part.n].attempts} intento(s)
:
{pUn ? 'Cuestionario de ' + S.PARTIAL.N + ' preguntas · aprobado al 50%' : 'Aprueba la parte anterior'}
}
{pUn && } {pUn && ( )}
); })} {/* final */} {(() => { const fUn = S.finalUnlocked(unit.slug); const fPassed = pr.final.passed; return (
Cuestionario final
{fPassed ?
Superado · mejor nota {notaStr(pr.final.best, S.FINAL.N)} · abre {idx + 1 < units.length ? units[idx + 1].ud : 'el final del curso'}
:
{fUn ? S.FINAL.N + ' preguntas · aprobado al 50% · abre la siguiente unidad' : 'Supera las 4 partes primero'}
}
{fUn && ( )}
); })()}
)}
); })}
{quiz && ( { S.recordResult(quiz.slug, quiz.partKey, score, total); refresh(); }} onClose={() => { setQuiz(null); refresh(); }} /> )} {expl && (
{expl.title}