feat(test): add functionality for drill, hard test module testing
This commit is contained in:
@ -1,7 +1,212 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuthStore } from "../../../stores/authStore";
|
||||
import type { Topic } from "../../../types/topic";
|
||||
import { api } from "../../../utils/api";
|
||||
import { ChoiceCard } from "../../../components/ChoiceCard";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { slideVariants } from "../../../lib/utils";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type Step = "topic" | "review";
|
||||
|
||||
export const Drills = () => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [direction, setDirection] = useState<1 | -1>(1);
|
||||
|
||||
const [topics, setTopics] = useState<Topic[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [selectedTopics, setSelectedTopics] = useState<Topic[]>([]);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [step, setStep] = useState<Step>("topic");
|
||||
|
||||
const { storeTopics, setMode, setQuestionCount } = useExamConfigStore();
|
||||
|
||||
const toggleTopic = (topic: Topic) => {
|
||||
setSelectedTopics((prev) => {
|
||||
const exists = prev.some((t) => t.id === topic.id);
|
||||
|
||||
if (exists) {
|
||||
return prev.filter((t) => t.id !== topic.id);
|
||||
}
|
||||
|
||||
return [...prev, topic];
|
||||
});
|
||||
};
|
||||
|
||||
function handleStartDrill() {
|
||||
if (!user || !topics) return;
|
||||
|
||||
navigate(`/student/practice/${topics[0].id}/test`, { replace: true });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAllTopics = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const authStorage = localStorage.getItem("auth-storage");
|
||||
if (!authStorage) return;
|
||||
|
||||
const parsed = JSON.parse(authStorage) as {
|
||||
state?: { token?: string };
|
||||
};
|
||||
|
||||
const token = parsed.state?.token;
|
||||
if (!token) return;
|
||||
|
||||
const response = await api.fetchAllTopics(token);
|
||||
setTopics(response);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to load topics. Reason: " + error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAllTopics();
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8 space-y-4">
|
||||
Drills
|
||||
<header className="space-y-2">
|
||||
<h1 className="font-satoshi-bold text-3xl">Drills</h1>
|
||||
<p className="font-satoshi text-md text-gray-500">
|
||||
Train your speed and accuracy with our drill-based testing system.
|
||||
</p>
|
||||
</header>
|
||||
<section>
|
||||
<div className="relative overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{step === "topic" && (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
key="topic"
|
||||
variants={slideVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
className="space-y-4"
|
||||
>
|
||||
<h2 className="text-xl font-satoshi-bold">Choose a topic</h2>
|
||||
|
||||
<input
|
||||
placeholder="Search topics..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full rounded-xl border px-4 py-2"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{loading ? (
|
||||
<>
|
||||
<div>
|
||||
<Loader2
|
||||
size={30}
|
||||
color="purple"
|
||||
className="animate-spin"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
topics
|
||||
.filter((t) =>
|
||||
t.name.toLowerCase().includes(search.toLowerCase()),
|
||||
)
|
||||
.map((t) => (
|
||||
<ChoiceCard
|
||||
key={t.id}
|
||||
label={t.name}
|
||||
subLabel={t.parent_name}
|
||||
section={t.section}
|
||||
selected={selectedTopics.some((st) => st.id === t.id)}
|
||||
onClick={() => toggleTopic(t)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
disabled={selectedTopics.length === 0}
|
||||
onClick={() => {
|
||||
// ✅ STORE
|
||||
storeTopics(selectedTopics.map((t) => t.id)); // ✅ STORE
|
||||
setMode("DRILL"); // ✅ STORE
|
||||
setQuestionCount(7); // ✅ STORE
|
||||
setDirection(1);
|
||||
setStep("review");
|
||||
}}
|
||||
className={`rounded-2xl py-3 px-6 font-satoshi-bold transition
|
||||
${
|
||||
selectedTopics.length === 0
|
||||
? "bg-gray-300 text-gray-500 cursor-not-allowed"
|
||||
: "bg-linear-to-br from-purple-500 to-purple-600 text-white"
|
||||
}`}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{step === "review" && (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
key="review"
|
||||
variants={slideVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
className="space-y-6"
|
||||
>
|
||||
<h2 className="text-xl font-satoshi-bold">
|
||||
Review your choices
|
||||
</h2>
|
||||
|
||||
<div className="rounded-2xl border p-4 space-y-2 font-satoshi">
|
||||
<p>
|
||||
<strong>Topics:</strong>{" "}
|
||||
{selectedTopics.map((t) => t.name).join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<button
|
||||
disabled={step === "topic"}
|
||||
onClick={() => {
|
||||
const order: Step[] = ["topic", "review"];
|
||||
setDirection(-1);
|
||||
setStep(order[order.indexOf(step) - 1]);
|
||||
}}
|
||||
className={`absolute bottom-24 left-10 rounded-2xl py-3 px-6 font-satoshi-bold transition
|
||||
${
|
||||
step === "topic"
|
||||
? "opacity-0 pointer-events-none"
|
||||
: "bg-linear-to-br from-slate-500 to-slate-600 text-white"
|
||||
}`}
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
|
||||
<button
|
||||
disabled={step !== "review"}
|
||||
className={`absolute bottom-24 right-10 rounded-2xl py-3 px-6 font-satoshi-bold transition
|
||||
${
|
||||
step !== "review"
|
||||
? "opacity-0 pointer-events-none"
|
||||
: "bg-linear-to-br from-purple-500 to-purple-600 text-white"
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleStartDrill();
|
||||
}}
|
||||
>
|
||||
Start Test
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user