feat(results): add resutls page

fix(leaderboard): fix leaderboard fetch logic

fix(test): fix navigation bug upon test quit
This commit is contained in:
shafin-r
2026-02-10 19:32:46 +06:00
parent 8cfcb11f0a
commit 7f82e640e0
17 changed files with 560 additions and 82 deletions

View File

@ -1,12 +1,159 @@
import { useNavigate } from "react-router-dom";
import { Button } from "../../../components/ui/button";
import { useResults } from "../../../stores/useResults";
import { useSatExam } from "../../../stores/useSatExam";
import { LucideArrowLeft } from "lucide-react";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { Progress } from "../../../components/ui/progress";
import { CircularLevelProgress } from "../../../components/CircularLevelProgress";
import { useEffect, useState } from "react";
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
const XPGainedCard = ({
results,
}: {
results?: {
xp_gained: number;
total_xp: number;
current_level_start: number;
next_level_threshold: number;
current_level: number;
};
}) => {
const [displayXP, setDisplayXP] = useState(0);
useEffect(() => {
if (!results?.xp_gained) return;
let startTime: number | null = null;
const duration = 800; // ms
const animate = (time: number) => {
if (!startTime) startTime = time;
const t = Math.min((time - startTime) / duration, 1);
setDisplayXP(Math.floor(t * results.xp_gained));
if (t < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [results?.xp_gained]);
return (
<Card>
<CardHeader>
<CardTitle>XP</CardTitle>
<CardDescription>How much did you improve?</CardDescription>
<CardAction>
<p className="font-satoshi-medium">+{displayXP} XP</p>
</CardAction>
</CardHeader>
</Card>
);
};
export const Results = () => {
const navigate = useNavigate();
const results = useResults((s) => s.results);
const clearResults = useResults((s) => s.clearResults);
const { setUserXp } = useExamConfigStore();
useEffect(() => {
if (results) setUserXp(results?.total_xp);
}, [results]);
function handleFinishExam() {
clearResults();
navigate(`/student/home`);
}
// const [displayXP, setDisplayXP] = useState(0);
// useEffect(() => {
// if (!results?.score) return;
// let start = 0;
// const duration = 600;
// const startTime = performance.now();
// const animate = (time: number) => {
// const t = Math.min((time - startTime) / duration, 1);
// setDisplayXP(Math.floor(t * results.score));
// if (t < 1) requestAnimationFrame(animate);
// };
// requestAnimationFrame(animate);
// }, [results?.score]);
const previousXP = results ? results.total_xp - results.xp_gained : 0;
return (
<div className="min-h-screen flex items-center justify-center text-2xl font-satoshi-bold">
Your results go here
<Button onClick={() => navigate("/student/home")}>Go to home</Button>
</div>
<main className="min-h-screen bg-gray-50 space-y-6 mx-auto px-8 sm:px-6 lg:px-90 py-10">
<header className="flex gap-4">
<button
onClick={() => handleFinishExam()}
className="p-2 rounded-full border border-purple-400 bg-linear-to-br from-purple-400 to-purple-500"
>
<LucideArrowLeft size={20} color="white" />
</button>
<h1 className="text-3xl font-satoshi-bold">Results</h1>
</header>
<section className="w-full flex items-center justify-center">
{results && (
<CircularLevelProgress
// previousXP={505}
// gainedXP={605}
// levelMinXP={500}
// levelMaxXP={1000}
// level={3}
previousXP={previousXP}
gainedXP={results.xp_gained}
levelMinXP={results.current_level_start}
levelMaxXP={results.next_level_threshold}
level={results.current_level}
/>
)}
</section>
<XPGainedCard results={results} />
<Card>
<CardHeader>
<CardTitle>Score</CardTitle>
<CardDescription>Total score you achieved.</CardDescription>
<CardAction>
<p className="font-satoshi-medium">{results?.score}</p>
</CardAction>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>Accuracy</CardTitle>
<CardDescription>How many did you answer correct?</CardDescription>
<CardAction>
<p className="font-satoshi-medium">
{results && results.total_questions > 0
? `${Math.round(
(results.correct_count / results.total_questions) * 100,
)}%`
: "—"}
</p>
</CardAction>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>How do you improve?</CardTitle>
<CardDescription>
Your score is good, but you can do better!
</CardDescription>
</CardHeader>
</Card>
</main>
);
};