feat(results): add resutls page
fix(leaderboard): fix leaderboard fetch logic fix(test): fix navigation bug upon test quit
This commit is contained in:
@ -35,8 +35,6 @@ export const Home = () => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// const logout = useAuthStore((state) => state.logout);
|
||||
// const navigate = useNavigate();
|
||||
const [practiceSheets, setPracticeSheets] = useState<PracticeSheet[]>([]);
|
||||
const [notStartedSheets, setNotStartedSheets] = useState<PracticeSheet[]>([]);
|
||||
const [inProgressSheets, setInProgressSheets] = useState<PracticeSheet[]>([]);
|
||||
@ -92,7 +90,7 @@ export const Home = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50 space-y-6 max-w-full mx-auto px-8 sm:px-6 lg:px-8 py-12">
|
||||
<main className="min-h-screen bg-gray-50 space-y-6 max-w-full mx-auto px-8 sm:px-6 lg:px-90 py-12">
|
||||
<h1 className="text-4xl font-satoshi-bold tracking-tight text-gray-800 text-center">
|
||||
Welcome, {user?.name || "Student"}
|
||||
</h1>
|
||||
@ -103,7 +101,7 @@ export const Home = () => {
|
||||
your scores now!
|
||||
</p>
|
||||
</section> */}
|
||||
<Card className="relative bg-linear-to-br from-red-600 to-red-700 rounded-4xl">
|
||||
{/* <Card className="relative bg-linear-to-br from-red-600 to-red-700 rounded-4xl">
|
||||
<div className="space-y-4">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="font-satoshi-bold tracking-tight text-3xl text-white">
|
||||
@ -142,7 +140,7 @@ export const Home = () => {
|
||||
<div className="overflow-hidden opacity-30 -rotate-45 absolute -top-2 -right-30 ">
|
||||
<DecimalsArrowRight size={380} color="white" />
|
||||
</div>
|
||||
</Card>
|
||||
</Card> */}
|
||||
<h1 className="font-satoshi-bold text-2xl tracking-tight">
|
||||
What are you looking for?
|
||||
</h1>
|
||||
|
||||
@ -24,6 +24,7 @@ export const Practice = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const userXp = useExamConfigStore.getState().userXp;
|
||||
console.log(userXp);
|
||||
return (
|
||||
<main className="h-fit max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8 space-y-4">
|
||||
<header className="flex justify-between items-center">
|
||||
|
||||
@ -67,6 +67,7 @@ export const Rewards = () => {
|
||||
const response = await api.fetchLeaderboard(token);
|
||||
|
||||
setLeaderboard(response);
|
||||
|
||||
setUserXp(response.user_rank.total_xp);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
@ -320,13 +321,15 @@ export const Rewards = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
{isTopThree ? (
|
||||
<img
|
||||
src={trophies[leaderboard?.user_rank?.rank ?? Infinity]}
|
||||
src={
|
||||
trophies[(leaderboard?.user_rank?.rank ?? Infinity) - 1]
|
||||
}
|
||||
alt={`trophy_${leaderboard?.user_rank?.rank ?? Infinity}`}
|
||||
className="w-12 h-12"
|
||||
/>
|
||||
) : (
|
||||
<span className="w-12 text-center font-satoshi-bold text-white">
|
||||
{leaderboard?.user_rank?.rank ?? Infinity}
|
||||
{(leaderboard?.user_rank?.rank ?? Infinity) - 1}
|
||||
</span>
|
||||
)}
|
||||
<Avatar className={`p-6 ${getRandomColor()}`}>
|
||||
|
||||
@ -13,11 +13,11 @@ import {
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
||||
import { useSatExam } from "../../../stores/useSatExam";
|
||||
|
||||
export const Pretest = () => {
|
||||
const { setSheetId, setMode, storeDuration, setQuestionCount } =
|
||||
useExamConfigStore();
|
||||
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const { sheetId } = useParams<{ sheetId: string }>();
|
||||
const [carouselApi, setCarouselApi] = useState<CarouselApi>();
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Navigate, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@ -39,6 +39,7 @@ import {
|
||||
} from "../../../components/ui/dialog";
|
||||
import { useExamNavigationGuard } from "../../../hooks/useExamNavGuard";
|
||||
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
||||
import { useResults } from "../../../stores/useResults";
|
||||
|
||||
export const Test = () => {
|
||||
const sheetId = localStorage.getItem("activePracticeSheetId");
|
||||
@ -79,6 +80,8 @@ export const Test = () => {
|
||||
const goToQuestion = useSatExam((s) => s.goToQuestion);
|
||||
|
||||
const finishExam = useSatExam((s) => s.finishExam);
|
||||
const quitExam = useSatExam((s) => s.quitExam);
|
||||
const setResults = useResults((s) => s.setResults);
|
||||
|
||||
const startExam = async () => {
|
||||
if (!user || !sheetId) return;
|
||||
@ -181,6 +184,11 @@ export const Test = () => {
|
||||
|
||||
if (next?.status === "COMPLETED") {
|
||||
useExamConfigStore.getState().clearPayload();
|
||||
console.log(next.results);
|
||||
setResults(next.results);
|
||||
|
||||
// ✅ Store results first
|
||||
|
||||
finishExam();
|
||||
} else {
|
||||
await loadSessionQuestions(sessionId);
|
||||
@ -190,14 +198,20 @@ export const Test = () => {
|
||||
|
||||
const handleQuitExam = () => {
|
||||
useExamConfigStore.getState().clearPayload();
|
||||
finishExam();
|
||||
navigate("/student/home");
|
||||
|
||||
quitExam();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
resetExam(); // ✅ important
|
||||
}, [sheetId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resetExam();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (phase === "FINISHED") {
|
||||
const timer = setTimeout(() => {
|
||||
@ -402,7 +416,7 @@ export const Test = () => {
|
||||
{currentQuestion?.context && (
|
||||
<section className="h-100 overflow-y-auto px-10 pt-30">
|
||||
<p className="font-satoshi tracking-wide text-lg">
|
||||
{currentQuestion?.context}
|
||||
{renderQuestionText(currentQuestion?.context)}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
@ -611,6 +625,8 @@ export const Test = () => {
|
||||
<p className="text-lg text-gray-500">Redirecting to results...</p>
|
||||
</div>
|
||||
);
|
||||
case "QUIT":
|
||||
return <Navigate to="/student/home" replace />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
|
||||
@ -17,8 +17,8 @@ export const TargetedPractice = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
storeTopics,
|
||||
setDifficulty: storeDifficulty,
|
||||
storeDuration,
|
||||
setDifficulty: storeDifficulty,
|
||||
setMode,
|
||||
setQuestionCount,
|
||||
} = useExamConfigStore();
|
||||
@ -34,7 +34,6 @@ export const TargetedPractice = () => {
|
||||
const [difficulty, setDifficulty] = useState<
|
||||
"EASY" | "MEDIUM" | "HARD" | null
|
||||
>(null);
|
||||
const [duration, setDuration] = useState<number | null>(null);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
@ -43,8 +42,6 @@ export const TargetedPractice = () => {
|
||||
|
||||
const difficulties = ["EASY", "MEDIUM", "HARD"] as const;
|
||||
|
||||
const durations = [10, 20, 30, 45];
|
||||
|
||||
const toggleTopic = (topic: Topic) => {
|
||||
setSelectedTopics((prev) => {
|
||||
const exists = prev.some((t) => t.id === topic.id);
|
||||
@ -58,7 +55,9 @@ export const TargetedPractice = () => {
|
||||
};
|
||||
|
||||
async function handleStartTargetedPractice() {
|
||||
if (!user || !token || !topics || !difficulty || !duration) return;
|
||||
if (!user || !token || !topics || !difficulty) return;
|
||||
|
||||
storeDuration(10);
|
||||
|
||||
navigate(`/student/practice/${topics[0].id}/test`, { replace: true });
|
||||
}
|
||||
@ -193,36 +192,6 @@ export const TargetedPractice = () => {
|
||||
setDifficulty(d); // local UI
|
||||
storeDifficulty(d); // ✅ STORE
|
||||
setDirection(1);
|
||||
setStep("duration");
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{step === "duration" && (
|
||||
<motion.div
|
||||
key="duration"
|
||||
custom={direction}
|
||||
variants={slideVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
className="space-y-4"
|
||||
>
|
||||
<h2 className="text-xl font-satoshi-bold">Select duration</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
{durations.map((d) => (
|
||||
<ChoiceCard
|
||||
key={d}
|
||||
label={`${d} minutes`}
|
||||
selected={duration === d}
|
||||
onClick={() => {
|
||||
setDuration(d);
|
||||
storeDuration(d); // ✅ STORE
|
||||
setDirection(1);
|
||||
setStep("review");
|
||||
}}
|
||||
/>
|
||||
@ -252,9 +221,6 @@ export const TargetedPractice = () => {
|
||||
<p>
|
||||
<strong>Difficulty:</strong> {difficulty}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Duration:</strong> {duration} minutes
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -263,7 +229,7 @@ export const TargetedPractice = () => {
|
||||
<button
|
||||
disabled={step === "topic"}
|
||||
onClick={() => {
|
||||
const order: Step[] = ["topic", "difficulty", "duration", "review"];
|
||||
const order: Step[] = ["topic", "difficulty", "review"];
|
||||
setDirection(-1);
|
||||
setStep(order[order.indexOf(step) - 1]);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user