// Quiz screen + Glossary + Mural + Trophies + Videos const { useState: useStateQ, useEffect: useEffectQ, useMemo, useRef: useRefQ } = React; // Reusable nav header for secondary screens — back to map + jump to any module function NavHeader({ title, icon, onClose, current, onNav }) { const tabs = [ { id: 'map', icon: '🗺️', label: 'Mapa' }, { id: 'videos', icon: '🎬', label: 'Vídeos' }, { id: 'glossary', icon: '📖', label: 'Dicionário' }, { id: 'trophies', icon: '🏆', label: 'Troféus' }, { id: 'mural', icon: '👥', label: 'Mural' }, ]; return (

{icon} {title}

{tabs.map(t => ( ))}
); } function arraysEqual(a, b) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; const A = [...a].sort((x, y) => x - y); const B = [...b].sort((x, y) => x - y); return A.every((v, i) => v === B[i]); } function isCorrect(q, answer) { if (q.type === 'mc') return answer === q.correct; if (q.type === 'vf') { if (!Array.isArray(answer) || answer.length !== q.correct.length) return false; return q.correct.every((v, i) => v === answer[i]); } if (q.type === 'multi') return arraysEqual(q.correct, Array.isArray(answer) ? answer : []); return false; } function Confetti({ count = 60, big = false }) { const pieces = Array.from({ length: count }); const colors = ['#fbbf24', '#22d3ee', '#fb7185', '#a78bfa', '#4ade80', '#f97316', '#f472b6']; const shapes = ['square', 'circle', 'star']; return ( ); } function CelebrationBurst({ kind = 'good' }) { // Random celebratory message + emoji combo const goodMessages = [ { msg: 'INCRÍVEL!', emoji: '🎉' }, { msg: 'MANDOU BEM!', emoji: '⭐' }, { msg: 'FANTÁSTICO!', emoji: '🚀' }, { msg: 'PERFEITO!', emoji: '✨' }, { msg: 'ARRASOU!', emoji: '🌟' }, { msg: 'GENIAL!', emoji: '💫' }, { msg: 'BOOM!', emoji: '💥' }, { msg: 'UAU!', emoji: '🎊' } ]; const pick = useMemo(() => goodMessages[Math.floor(Math.random() * goodMessages.length)], []); if (kind !== 'good') return null; return (
{pick.emoji}
{pick.msg}
); } // Fisher-Yates shuffle (returns new array) function shuffled(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } // ----- Quiz Screen ----- function QuizScreen({ user, mission, progress, onExit, onComplete, onOpenGlossary }) { const [queue, setQueue] = useStateQ(() => shuffled(mission.questions.map((_, i) => i))); const [pos, setPos] = useStateQ(0); const [confirmExit, setConfirmExit] = useStateQ(false); const [answer, setAnswer] = useStateQ(null); const [submitted, setSubmitted] = useStateQ(false); const [correct, setCorrect] = useStateQ(false); const [results, setResults] = useStateQ({}); const [showSummary, setShowSummary] = useStateQ(false); const [shake, setShake] = useStateQ(false); const [streak, setStreak] = useStateQ(0); const [showCelebration, setShowCelebration] = useStateQ(false); const [resumed, setResumed] = useStateQ(false); const total = mission.questions.length; // ----- Resume on mount ----- useEffectQ(() => { const mp = progress.missions[mission.id]; const r = mp?.resume; if (r && !mp?.completed && r.results && r.queue && r.pos != null) { setQueue(r.queue); setPos(r.pos); setResults(r.results); setStreak(r.streak || 0); setResumed(true); } // eslint-disable-next-line }, []); const currentIdx = queue[pos]; const q = currentIdx != null ? mission.questions[currentIdx] : null; // Reset answer per question useEffectQ(() => { if (!q) return; if (q.type === 'mc') setAnswer(null); else if (q.type === 'vf') setAnswer(q.statements.map(() => null)); else if (q.type === 'multi') setAnswer([]); setSubmitted(false); setCorrect(false); }, [pos, queue]); // ----- Persist resume state ----- useEffectQ(() => { if (showSummary) return; const mp = { ...(progress.missions[mission.id] || {}) }; mp.resume = { queue, pos, results, streak, answeredCount: Object.keys(results).length }; const newProg = { ...progress, missions: { ...progress.missions, [mission.id]: mp } }; window.AppStorage.saveProgress(user.id, newProg); // eslint-disable-next-line }, [pos, results, streak]); function canSubmit() { if (!q) return false; if (q.type === 'mc') return answer !== null; if (q.type === 'vf') return Array.isArray(answer) && answer.every(v => v !== null); if (q.type === 'multi') return Array.isArray(answer) && answer.length === q.pickCount; return false; } function handleSubmit() { if (!canSubmit() || submitted) return; const ok = isCorrect(q, answer); setSubmitted(true); setCorrect(ok); setResults(prev => { const old = prev[currentIdx] || { correct: false, attempts: 0 }; return { ...prev, [currentIdx]: { correct: ok || old.correct, attempts: old.attempts + 1 } }; }); if (ok) { setStreak(s => s + 1); setShowCelebration(true); setTimeout(() => setShowCelebration(false), 1400); } else { setStreak(0); setShake(true); setTimeout(() => setShake(false), 500); } } function handleNext() { if (pos + 1 < queue.length) { setPos(pos + 1); } else { // Clear resume state when reaching summary const mp = { ...(progress.missions[mission.id] || {}) }; mp.resume = null; const newProg = { ...progress, missions: { ...progress.missions, [mission.id]: mp } }; window.AppStorage.saveProgress(user.id, newProg); setShowSummary(true); // Scroll to top so users see the hero window.scrollTo({ top: 0, behavior: 'instant' }); } } function retryWrong() { const wrong = mission.questions.map((_, i) => i).filter(i => !results[i]?.correct); if (wrong.length === 0) return; setQueue(shuffled(wrong)); setPos(0); setShowSummary(false); } // Compute stars: 1 ponto base por acerto, -0.34 por tentativa extra. >=80% pct sem retries = 3⭐, etc. function computeStars() { const correctCount = mission.questions.filter((_, i) => results[i]?.correct).length; const totalAttempts = mission.questions.reduce((s, _, i) => s + (results[i]?.attempts || 0), 0); const extraAttempts = Math.max(0, totalAttempts - total); // erros + retries const pct = correctCount / total; if (pct < 0.6) return 0; // Penaliza: cada erro/retry tira pontos. 0 erros = 3, 1–2 erros = 2, 3+ erros = 1 if (extraAttempts === 0 && pct >= 1) return 3; if (extraAttempts <= 2 && pct >= 0.8) return 2; return 1; } function finishMission() { const correctCount = mission.questions.filter((_, i) => results[i]?.correct).length; const totalAttempts = mission.questions.reduce((s, _, i) => s + (results[i]?.attempts || 0), 0); const stars = computeStars(); onComplete({ stars, correctCount, total, results, totalAttempts }); } // ----- Summary ----- if (showSummary) { const correctCount = mission.questions.filter((_, i) => results[i]?.correct).length; const totalAttempts = mission.questions.reduce((s, _, i) => s + (results[i]?.attempts || 0), 0); const extraAttempts = Math.max(0, totalAttempts - total); const pct = correctCount / total; const stars = computeStars(); const wrongIdxs = mission.questions.map((_, i) => i).filter(i => !results[i]?.correct); // Compute simulated overall progress (this mission counts at the new stars level) const simulatedMissions = window.MISSIONS.map(mm => { if (mm.id === mission.id) { const prevStars = progress.missions[mm.id]?.stars || 0; return { ...mm, stars: Math.max(prevStars, stars), completed: true }; } const mp = progress.missions[mm.id]; return { ...mm, stars: mp?.stars || 0, completed: !!mp?.completed }; }); const totalStarsAcc = simulatedMissions.reduce((s, m) => s + m.stars, 0); const maxStarsAcc = window.MISSIONS.length * 3; const completedCount = simulatedMissions.filter(m => m.completed).length; const earnedBadges = new Set(progress.badges || []); if (stars > 0) earnedBadges.add(`mission_${mission.id}`); if (stars === 3) earnedBadges.add('mestre_cosmico'); const allThree = simulatedMissions.every(m => m.stars === 3); if (allThree) earnedBadges.add('lenda_cosmica'); const isNewBest = stars > (progress.missions[mission.id]?.stars || 0); const nextMissionIdx = window.MISSIONS.findIndex(m => m.id === mission.id) + 1; const nextMission = nextMissionIdx < window.MISSIONS.length ? window.MISSIONS[nextMissionIdx] : null; const justUnlockedNext = stars > 0 && nextMission && !(progress.missions[nextMission.id]?.stars > 0); const headlineMsg = stars === 3 ? { text: 'MISSÃO PERFEITA!', sub: 'Você é uma LENDA do espaço! 🌟', cls: 'great', emoji: '🏆' } : stars === 2 ? { text: 'EXCELENTE, CADETE!', sub: 'Quase perfeito! Continue assim! 🎉', cls: 'good', emoji: '🎉' } : stars === 1 ? { text: 'MISSÃO CONCLUÍDA!', sub: 'Bom trabalho! Tente de novo pra brilhar mais! 💪', cls: 'ok', emoji: '🚀' } : { text: 'NÃO DESISTA!', sub: 'Cada erro é um passo pro acerto! Bora de novo! 💪', cls: 'low', emoji: '🌠' }; return (
{stars > 0 && } {stars === 3 && }
{/* HERO */}
{headlineMsg.emoji}

{headlineMsg.text}

{headlineMsg.sub}

{mission.icon} {mission.title}
{[0, 1, 2].map(i => ( ))}
{isNewBest && stars > 0 && (
✨ NOVO RECORDE! ✨
)} {extraAttempts > 0 && stars < 3 && (
💡 {extraAttempts === 1 ? '1 erro' : `${extraAttempts} erros`} no caminho · Acerte tudo de primeira para 3⭐!
)}
{/* STATS GRID */}
🎯
{correctCount}/{total}
Acertos
{totalStarsAcc}/{maxStarsAcc}
Estrelas Totais
🌍
{completedCount}/{window.MISSIONS.length}
Missões
🏅
{earnedBadges.size}
Medalhas
{/* UNLOCK CARD */} {justUnlockedNext && (
✨🔓✨
Você destravou:
{nextMission.icon} {nextMission.title}
)} {/* MISSIONS PATH */}

🗺️ Sua Jornada

{simulatedMissions.map((mm, i) => (
0 ? 'done' : ''}`}>
{mm.stars > 0 ? mm.icon : '🔒'}
{[0,1,2].map(s => )}
))}
{/* ACTIONS */}
{wrongIdxs.length > 0 && ( )}
); } if (!q) return null; const progressPct = ((pos + (submitted ? 1 : 0)) / queue.length) * 100; return (
{mission.icon} {mission.title}
{confirmExit && (
setConfirmExit(false)}>
e.stopPropagation()}>
🚀

Sair da missão?

Não se preocupe! Seu progresso fica salvo e você pode continuar de onde parou quando voltar.

Progresso desta missão:
{Object.keys(results).length} de {total} respondidas
)} {resumed && (
▶ Continuando de onde você parou!
)}
🚀
{pos + 1}/{queue.length}
{streak >= 2 && (
🔥 Sequência de {streak}!
)}
Pergunta {pos + 1}

{q.prompt}

{q.type === 'mc' && (
{q.options.map((opt, i) => { let cls = 'option'; if (submitted) { if (correct && i === q.correct) cls += ' correct'; else if (i === answer && !correct) cls += ' wrong'; } else if (answer === i) cls += ' selected'; return ( ); })}
)} {q.type === 'vf' && (
{q.statements.map((s, i) => { const cur = Array.isArray(answer) ? answer[i] : null; const showResult = submitted; const isRight = q.correct[i]; return (
{s}
); })}
)} {q.type === 'multi' && (
Marque {q.pickCount} opções • Selecionou: {(Array.isArray(answer) ? answer : []).length}/{q.pickCount}
{q.options.map((opt, i) => { const sel = (Array.isArray(answer) ? answer : []).includes(i); const isAnsCorrect = q.correct.includes(i); let cls = 'option'; if (submitted) { if (correct && isAnsCorrect) cls += ' correct'; else if (sel && !correct) cls += ' wrong'; } else if (sel) cls += ' selected'; return ( ); })}
)} {submitted && (
{correct ? '🎉 Mandou bem, Cadete!' : '🤔 Hmm, não foi dessa vez!'} {correct ? (

{q.explain}

) : ( <>

📘 Vamos aprender: {q.explain}

💡 Dica: revise os vídeos da Profª. e o Dicionário Espacial 📖 para fixar o conteúdo. Depois, use "🔄 Treinar erradas" no fim da missão!

)}
)}
{!submitted ? ( ) : ( )}
{showCelebration && ( <> )}
); } // ----- Glossary ----- function GlossaryScreen({ onClose, onView, onNav }) { const [search, setSearch] = useStateQ(''); const filtered = useMemo(() => { const s = search.toLowerCase().trim(); if (!s) return window.GLOSSARY; return window.GLOSSARY.filter(g => g.term.toLowerCase().includes(s) || g.def.toLowerCase().includes(s) ); }, [search]); useEffectQ(() => { if (onView) onView(); }, []); return (
setSearch(e.target.value)} />
{filtered.length === 0 &&
Nenhuma palavra encontrada.
} {filtered.map(g => (

{g.icon || '✨'} {g.term}

{g.def}

))}
); } // ----- Mural ----- function MuralScreen({ user, onClose, onNav }) { const [entries, setEntries] = useStateQ([]); const [loading, setLoading] = useStateQ(true); useEffectQ(() => { window.AppStorage.getMural().then(e => { setEntries(e || []); setLoading(false); }); }, []); function timeAgo(ts) { const diff = Date.now() - ts; const m = Math.floor(diff / 60000); if (m < 1) return 'agora'; if (m < 60) return `${m} min atrás`; const h = Math.floor(m / 60); if (h < 24) return `${h} h atrás`; return `${Math.floor(h / 24)} d atrás`; } return (

Conquistas dos colegas que escolheram compartilhar!

{loading &&
Carregando...
} {!loading && entries.length === 0 && (
📭

Nenhuma publicação ainda.

Termine uma missão e seja o(a) primeiro(a) a publicar!

)}
{entries.map((e, i) => { const av = window.Avatars.get(e.avatar); const Av = av.component; return (
{e.nick} {timeAgo(e.ts)}
Concluiu a missão {e.missionIcon} {e.missionTitle}
{e.correctCount}/{e.total} acertos
); })}
); } // ----- Trophies ----- function TrophiesScreen({ user, progress, onClose, onNav }) { const allBadges = window.BADGES; const earned = new Set(progress.badges || []); const totalStars = window.MISSIONS.reduce((sum, m) => sum + (Number.isFinite(progress.missions[m.id]?.stars) ? progress.missions[m.id].stars : 0), 0); const rank = window.Avatars.getRank(totalStars); const maxStars = window.MISSIONS.length * 3; const rankProgress = rank.next ? Math.round(((totalStars - rank.min) / (rank.next.min - rank.min)) * 100) : 100; return (

{user.nick}

{user.name}

{rank.emoji} Patente: {rank.title}
{rank.next && (
Próxima patente: {rank.next.emoji} {rank.next.title} — faltam {rank.next.min - totalStars}⭐
)}
⭐ {totalStars}/{maxStars}estrelas
🏅 {earned.size}medalhas

Medalhas

{allBadges.map(b => { const got = earned.has(b.id); return (
{got ? b.icon : '🔒'}
{b.name}

{got ? b.desc : b.hint}

); })}

Missões

{window.MISSIONS.map(m => { const mp = progress.missions[m.id]; const stars = mp?.stars || 0; return (
{m.icon} {m.title}
); })}
); } // ----- Videos ----- function VideoPlayer({ video }) { const [src, setSrc] = useStateQ(null); useEffectQ(() => { let revoke = null; if (video.kind === 'file') { // Modo API: usa fileUrl direto. Modo local: busca blob no IndexedDB. if (video.fileUrl) { setSrc(video.fileUrl); } else if (video.blobId) { window.AppStorage.getVideoBlob(video.blobId).then(blob => { if (blob) { const url = URL.createObjectURL(blob); revoke = url; setSrc(url); } }); } } return () => { if (revoke) URL.revokeObjectURL(revoke); }; }, [video.id]); const ytId = video.youtubeId || video.youtube; if (video.kind === 'youtube' && ytId) { return (
); } if (video.kind === 'file') { return (
{src ? (
); } return null; } function VideosScreen({ onClose, onNav }) { const [videos, setVideos] = useStateQ([]); const [loading, setLoading] = useStateQ(true); const [playing, setPlaying] = useStateQ(null); useEffectQ(() => { window.AppStorage.getVideos().then(v => { setVideos(v || []); setLoading(false); }); }, []); function fmtSize(bytes) { if (!bytes) return ''; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB'; return (bytes / 1024 / 1024).toFixed(1) + ' MB'; } return (

Assista os vídeos da Profª. para reforçar o aprendizado!

{loading &&
Carregando...
} {!loading && videos.length === 0 && (
🎥

Nenhum vídeo ainda!

A Profª. ainda não adicionou os vídeos. Volte depois!

)}
{videos.map(v => (
{playing === v.id ? ( ) : (
setPlaying(v.id)}> {(v.kind === 'youtube' && (v.youtubeId || v.youtube)) ? ( {v.title} ) : (
{v.kind === 'file' ? '📁🎬' : '🎬'}
)}
{v.kind === 'file' &&
📁 Arquivo
} {v.kind === 'youtube' &&
▶ YouTube
}
)}

{v.title}

{(v.description || v.desc) &&

{v.description || v.desc}

} {v.kind === 'file' && (

📁 {v.fileName} • {fmtSize(v.fileSize)}

)}
{playing !== v.id && ( )} {playing === v.id && ( )} {v.kind === 'youtube' && ( 🔗 Abrir no YouTube )}
))}
); } // ----- Share dialog ----- function ShareDialog({ user, mission, result, onShare, onSkip }) { return (
{mission.icon}

Missão concluída!

Você quer publicar essa conquista no Mural da Turma?

{[0, 1, 2].map(i => ( ))}
); } window.QuizScreens = { QuizScreen, GlossaryScreen, MuralScreen, TrophiesScreen, VideosScreen, ShareDialog, isCorrect };