Files
anatomia/frontend/src/routes/QuizPage.tsx
T

156 lines
5.5 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { api, type Structure, type QuizCard } from "../lib/api.js";
import { useSystemStructures } from "../hooks/useSystemStructures.js";
import { Scene } from "../components/viewer/Scene.js";
import { LayerToggle } from "../components/LayerToggle.js";
import { useAtlasStore } from "../store/useAtlasStore.js";
type Deck = (Structure & { quizCard: QuizCard | null })[];
export default function QuizPage() {
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
const { bySlug } = useSystemStructures(visibleSystems);
const [deck, setDeck] = useState<Deck>([]);
const [index, setIndex] = useState(0);
const [feedback, setFeedback] = useState<"correct" | "wrong" | null>(null);
const [score, setScore] = useState({ correct: 0, total: 0 });
const [layersOpen, setLayersOpen] = useState(false);
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const selectStructure = useAtlasStore((s) => s.selectStructure);
const setVisibleSystems = useAtlasStore((s) => s.setVisibleSystems);
const focusOnStructure = useAtlasStore((s) => s.focusOnStructure);
async function loadDeck() {
const next = await api.quizNext({ limit: 10 });
setDeck(next);
setIndex(0);
setScore({ correct: 0, total: 0 });
selectStructure(null);
if (next.length > 0) {
setVisibleSystems([...new Set(next.map((s) => s.system.slug))]);
}
}
useEffect(() => {
loadDeck();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const current = deck[index];
const ignoreNextSelectRef = useRef(false);
useEffect(() => {
if (!current || !selectedStructureId || feedback) return;
if (ignoreNextSelectRef.current) {
ignoreNextSelectRef.current = false;
return;
}
const isCorrect = selectedStructureId === current.id;
setFeedback(isCorrect ? "correct" : "wrong");
setScore((s) => ({ correct: s.correct + (isCorrect ? 1 : 0), total: s.total + 1 }));
api.quizReview(current.id, isCorrect ? 5 : 2).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedStructureId]);
function next() {
selectStructure(null);
setFeedback(null);
if (index + 1 < deck.length) setIndex(index + 1);
else loadDeck();
}
function giveUp() {
if (!current) return;
setFeedback("wrong");
setScore((s) => ({ ...s, total: s.total + 1 }));
api.quizReview(current.id, 2).catch(console.error);
ignoreNextSelectRef.current = true;
focusOnStructure(current.slug);
selectStructure(current.id);
}
return (
<div className="h-full relative md:grid md:grid-cols-[280px_1fr]">
<aside
className={`fixed md:static inset-y-0 left-0 z-30 w-[86vw] max-w-[300px] md:w-auto
border-r border-white/10 bg-panel p-3 flex flex-col gap-4 min-h-0 overflow-y-auto
transition-transform duration-200 md:translate-x-0
${layersOpen ? "translate-x-0" : "-translate-x-full"}`}
>
<button
onClick={() => setLayersOpen(false)}
className="md:hidden self-end text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar painel"
>
✕ Fechar
</button>
<LayerToggle />
</aside>
{layersOpen && (
<div
className="fixed inset-0 bg-black/50 z-20 md:hidden"
onClick={() => setLayersOpen(false)}
/>
)}
<div className="relative h-full min-h-0">
<Scene bySlug={bySlug} />
<button
onClick={() => setLayersOpen(true)}
className="md:hidden absolute top-3 left-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
>
☰ Camadas
</button>
<div className="absolute top-3 left-1/2 -translate-x-1/2 bg-panel/95 border border-white/10 rounded-lg px-3 py-2.5 sm:px-5 sm:py-3 text-center shadow-lg w-[80vw] sm:w-auto sm:max-w-md">
{!current ? (
<p className="text-sm text-slate-300">
Nenhuma estrutura pendente de revisão agora. 🎉
</p>
) : (
<>
<p className="text-xs text-slate-400 uppercase tracking-wide mb-1">
Onde fica esta estrutura?
</p>
<p className="text-base sm:text-lg font-semibold text-white">{current.namePt}</p>
<p className="text-xs text-slate-500 mb-2">{current.system.namePt}</p>
{feedback === "correct" && (
<p className="text-sm text-emerald-400 mb-2">Correto! ✅</p>
)}
{feedback === "wrong" && (
<p className="text-sm text-rose-400 mb-2">Não foi essa estrutura.</p>
)}
<div className="flex gap-2 justify-center mt-2">
{feedback ? (
<button
onClick={next}
className="text-sm px-3 py-1.5 rounded bg-accent text-white"
>
Próxima
</button>
) : (
<button
onClick={giveUp}
className="text-sm px-3 py-1.5 rounded bg-white/10 text-slate-200"
>
Mostrar resposta
</button>
)}
</div>
<p className="text-xs text-slate-500 mt-2">
{index + 1} / {deck.length} · acertos: {score.correct}/{score.total}
</p>
</>
)}
</div>
</div>
</div>
);
}