feat(targeted): add targeted practice functionality
feat(analytics); add analytics page
This commit is contained in:
71
src/pages/student/Analytics.tsx
Normal file
71
src/pages/student/Analytics.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { List, SquarePen, DecimalsArrowRight, MapPin } from "lucide-react";
|
||||
import { Progress } from "../../components/ui/progress";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from "../../components/ui/card";
|
||||
import { Field, FieldLabel } from "../../components/ui/field";
|
||||
import { CircularProgress } from "../../components/CircularProgress";
|
||||
|
||||
export const Analytics = () => {
|
||||
return (
|
||||
<main className="min-h-screen max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8 space-y-4">
|
||||
<h1 className="font-satoshi-bold text-3xl text-center tracking-tight">
|
||||
Analytics
|
||||
</h1>
|
||||
<section className="flex w-full gap-3 justify-between">
|
||||
<Card className="w-1/3 relative bg-linear-to-br from-purple-600 to-purple-700 rounded-4xl">
|
||||
<div className="space-y-4">
|
||||
<CardContent className="md:w-full space-y-4 flex flex-col items-center justify-center h-50">
|
||||
<MapPin size={60} color="white" />
|
||||
<h1 className="text-4xl font-satoshi-bold text-white flex">
|
||||
<span>145</span> <span className="text-xl">th</span>
|
||||
</h1>
|
||||
</CardContent>
|
||||
</div>
|
||||
<div className="overflow-hidden opacity-0 -rotate-45 absolute -top-2 -right-30 ">
|
||||
<DecimalsArrowRight size={380} color="white" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
className="w-2/3 relative bg-linear-to-br from-gray-100 to-gray-300 rounded-4xl
|
||||
flex-row"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<CardHeader className="md:w-full">
|
||||
<CardTitle className="font-satoshi-bold tracking-tight text-3xl ">
|
||||
Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="md:w-full space-y-4"></CardContent>
|
||||
<CardFooter className="flex justify-between"></CardFooter>
|
||||
</div>
|
||||
<div className="overflow-hidden opacity-30 -rotate-45 absolute -top-2 -right-30 ">
|
||||
<DecimalsArrowRight size={380} color="white" />
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
<section>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Field className="w-full max-w-sm">
|
||||
<FieldLabel htmlFor="progress-upload">
|
||||
<span className="font-satoshi text-xl">Score</span>
|
||||
<span className="ml-auto font-satoshi">
|
||||
<span className="text-5xl">854</span>
|
||||
<span className="text-lg">/1600</span>
|
||||
</span>
|
||||
</FieldLabel>
|
||||
<Progress value={55} id="progress-upload" max={100} />
|
||||
</Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@ -6,7 +6,14 @@ import {
|
||||
TabsContent,
|
||||
} from "../../components/ui/tabs";
|
||||
import { useAuthStore } from "../../stores/authStore";
|
||||
import { CheckCircle, Search } from "lucide-react";
|
||||
import {
|
||||
CheckCircle,
|
||||
DecimalsArrowRight,
|
||||
DraftingCompass,
|
||||
List,
|
||||
Search,
|
||||
SquarePen,
|
||||
} from "lucide-react";
|
||||
import { api } from "../../utils/api";
|
||||
import {
|
||||
Card,
|
||||
@ -21,6 +28,8 @@ import { Button } from "../../components/ui/button";
|
||||
import type { PracticeSheet } from "../../types/sheet";
|
||||
import { formatStatus } from "../../lib/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Progress } from "../../components/ui/progress";
|
||||
import { Field, FieldLabel } from "../../components/ui/field";
|
||||
|
||||
export const Home = () => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
@ -78,15 +87,68 @@ export const Home = () => {
|
||||
fetchPracticeSheets();
|
||||
}, [user]);
|
||||
|
||||
const handleStartPractice = (sheetId: string) => {
|
||||
const handleStartPracticeSheet = (sheetId: string) => {
|
||||
navigate(`/student/practice/${sheetId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50 flex flex-col gap-12 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-8 py-12">
|
||||
<h1 className="text-4xl font-satoshi-bold tracking-tight text-gray-800 text-center">
|
||||
Welcome, {user?.name || "Student"}
|
||||
</h1>
|
||||
|
||||
{/* <section className="border rounded-3xl p-5 space-y-4">
|
||||
<p className="font-satoshi">
|
||||
Your predictive SAT score is low. Take a practice test to increase
|
||||
your scores now!
|
||||
</p>
|
||||
</section> */}
|
||||
<Card
|
||||
className="relative bg-linear-to-br from-red-600 to-red-700 rounded-4xl
|
||||
flex-row"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<CardHeader className="w-[200%] md:w-full">
|
||||
<CardTitle className="font-satoshi-bold tracking-tight text-3xl text-white">
|
||||
Your score is low!
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="md:w-full space-y-4">
|
||||
<Field className="w-full max-w-sm">
|
||||
<FieldLabel htmlFor="progress-upload">
|
||||
<span className="font-satoshi text-white">Score</span>
|
||||
<span className="ml-auto font-satoshi text-white">
|
||||
854/1600
|
||||
</span>
|
||||
</FieldLabel>
|
||||
<Progress value={55} id="progress-upload" max={100} />
|
||||
</Field>
|
||||
<p className="font-satoshi text-white">
|
||||
Taking more practice tests can increase your score today!
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
onClick={() => navigate("/student/analytics")}
|
||||
className="bg-transparent border-2 py-3 px-5 text-md font-satoshi rounded-full"
|
||||
>
|
||||
<List />
|
||||
View
|
||||
</Button>
|
||||
<Button className="bg-gray-50 py-3 px-5 text-md font-satoshi text-black rounded-full">
|
||||
<SquarePen />
|
||||
Take a practice test
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</div>
|
||||
<div className="overflow-hidden opacity-30 -rotate-45 absolute -top-2 -right-30 ">
|
||||
<DecimalsArrowRight size={380} color="white" />
|
||||
</div>
|
||||
</Card>
|
||||
<h1 className="font-satoshi-bold text-2xl tracking-tight">
|
||||
What are you looking for?
|
||||
</h1>
|
||||
<section className="relative w-full">
|
||||
<input
|
||||
type="text"
|
||||
@ -133,7 +195,7 @@ export const Home = () => {
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
onClick={() => handleStartPractice(sheet?.id)}
|
||||
onClick={() => handleStartPracticeSheet(sheet?.id)}
|
||||
variant="outline"
|
||||
className="font-satoshi rounded-3xl w-full text-lg py-6 bg-linear-to-br from-purple-500 to-purple-600 text-white"
|
||||
>
|
||||
@ -203,7 +265,7 @@ export const Home = () => {
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
onClick={() => handleStartPractice(sheet?.id)}
|
||||
onClick={() => handleStartPracticeSheet(sheet?.id)}
|
||||
variant="outline"
|
||||
className="font-satoshi rounded-3xl w-full text-lg py-6 bg-linear-to-br from-purple-500 to-purple-600 text-white"
|
||||
>
|
||||
|
||||
@ -1,16 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Outlet, replace, useParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { api } from "../../../utils/api";
|
||||
import { useAuthStore } from "../../../stores/authStore";
|
||||
import type { PracticeSheet } from "../../../types/sheet";
|
||||
import {
|
||||
CircleQuestionMark,
|
||||
Clock,
|
||||
Layers,
|
||||
Loader,
|
||||
Loader2,
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
import { CircleQuestionMark, Clock, Layers, Loader, Tag } from "lucide-react";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
@ -19,8 +12,12 @@ import {
|
||||
} from "../../../components/ui/carousel";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
||||
|
||||
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>();
|
||||
@ -37,6 +34,12 @@ export const Pretest = () => {
|
||||
console.error("Sheet ID is required to start the test.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSheetId(sheetId);
|
||||
setMode("MODULE");
|
||||
storeDuration(practiceSheet?.time_limit ?? 0);
|
||||
setQuestionCount(2);
|
||||
|
||||
navigate(`/student/practice/${sheetId}/test`, { replace: true });
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@ -10,7 +10,7 @@ import { Check, Loader2 } from "lucide-react";
|
||||
|
||||
import { api } from "../../../utils/api";
|
||||
import { useAuthStore } from "../../../stores/authStore";
|
||||
import type { PracticeSheet, Question } from "../../../types/sheet";
|
||||
import type { Question } from "../../../types/sheet";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { useSatExam } from "../../../stores/useSatExam";
|
||||
import { useSatTimer } from "../../../hooks/useSatTimer";
|
||||
@ -38,8 +38,10 @@ import {
|
||||
DialogTrigger,
|
||||
} from "../../../components/ui/dialog";
|
||||
import { useExamNavigationGuard } from "../../../hooks/useExamNavGuard";
|
||||
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
||||
|
||||
export const Test = () => {
|
||||
const sheetId = localStorage.getItem("activePracticeSheetId");
|
||||
const blocker = useExamNavigationGuard();
|
||||
const [eliminated, setEliminated] = useState<Record<string, Set<string>>>({});
|
||||
|
||||
@ -54,15 +56,11 @@ export const Test = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuthStore();
|
||||
const token = useAuthToken();
|
||||
const [practiceSheet, setPracticeSheet] = useState<PracticeSheet | null>(
|
||||
null,
|
||||
);
|
||||
const [answers, setAnswers] = useState<Record<string, string>>({});
|
||||
const [showNavigator, setShowNavigator] = useState<boolean>(false);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const { sheetId } = useParams<{ sheetId: string }>();
|
||||
|
||||
const time = useSatTimer();
|
||||
const phase = useSatExam((s) => s.phase);
|
||||
@ -84,15 +82,10 @@ export const Test = () => {
|
||||
const startExam = async () => {
|
||||
if (!user || !sheetId) return;
|
||||
|
||||
const payload = useExamConfigStore.getState().payload;
|
||||
|
||||
try {
|
||||
const response = await api.startSession(token as string, {
|
||||
sheet_id: sheetId,
|
||||
mode: "MODULE",
|
||||
topic_ids: practiceSheet?.topics.map((t) => t.id) ?? [],
|
||||
difficulty: practiceSheet?.difficulty ?? "EASY",
|
||||
question_count: 2,
|
||||
time_limit_minutes: practiceSheet?.time_limit ?? 0,
|
||||
});
|
||||
const response = await api.startSession(token as string, payload);
|
||||
|
||||
setSessionId(response.id);
|
||||
|
||||
@ -185,6 +178,7 @@ export const Test = () => {
|
||||
const next = await api.fetchNextModule(token!, sessionId);
|
||||
|
||||
if (next?.status === "COMPLETED") {
|
||||
useExamConfigStore.getState().clearPayload();
|
||||
finishExam();
|
||||
} else {
|
||||
await loadSessionQuestions(sessionId);
|
||||
|
||||
@ -1,7 +1,327 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../../../utils/api";
|
||||
import { type Topic } from "../../../types/topic";
|
||||
import { useAuthStore } from "../../../stores/authStore";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { slideVariants } from "../../../lib/utils";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { useAuthToken } from "../../../hooks/useAuthToken";
|
||||
import type {
|
||||
TargetedSessionRequest,
|
||||
TargetedSessionResponse,
|
||||
} from "../../../types/session";
|
||||
|
||||
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
|
||||
import { replace, useNavigate } from "react-router-dom";
|
||||
|
||||
type Step = "topic" | "difficulty" | "duration" | "review";
|
||||
|
||||
const ChoiceCard = ({
|
||||
label,
|
||||
selected,
|
||||
subLabel,
|
||||
section,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
subLabel?: string;
|
||||
section?: string;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`rounded-2xl border p-4 text-left transition flex flex-col
|
||||
${selected ? "border-purple-600 bg-purple-50" : "hover:border-gray-300"}`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-satoshi-bold text-lg">{label}</span>
|
||||
{section && (
|
||||
<Badge
|
||||
variant={"secondary"}
|
||||
className={`font-satoshi text-sm ${section === "EBRW" ? "bg-blue-400 text-blue-100" : "bg-red-400 text-red-100"}`}
|
||||
>
|
||||
{section}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{subLabel && <span className="font-satoshi text-md">{subLabel}</span>}
|
||||
</button>
|
||||
);
|
||||
|
||||
export const TargetedPractice = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
storeTopics,
|
||||
setDifficulty: storeDifficulty,
|
||||
storeDuration,
|
||||
setMode,
|
||||
setQuestionCount,
|
||||
} = useExamConfigStore();
|
||||
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const token = useAuthToken();
|
||||
const [direction, setDirection] = useState<1 | -1>(1);
|
||||
|
||||
const [step, setStep] = useState<Step>("topic");
|
||||
|
||||
const [selectedTopics, setSelectedTopics] = useState<Topic[]>([]);
|
||||
|
||||
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);
|
||||
|
||||
const [topics, setTopics] = useState<Topic[]>([]);
|
||||
|
||||
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);
|
||||
|
||||
if (exists) {
|
||||
return prev.filter((t) => t.id !== topic.id);
|
||||
}
|
||||
|
||||
return [...prev, topic];
|
||||
});
|
||||
};
|
||||
|
||||
async function handleStartTargetedPractice() {
|
||||
if (!user || !token || !topics || !difficulty || !duration) 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">
|
||||
Targeted Practice
|
||||
<main className="relative min-h-screen max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8 space-y-4">
|
||||
<h1 className="font-satoshi-bold text-3xl">Targeted Practice</h1>
|
||||
|
||||
<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={() => {
|
||||
setTopics(selectedTopics.map((t) => t.id)); // ✅ STORE
|
||||
storeTopics(selectedTopics.map((t) => t.id)); // ✅ STORE
|
||||
setMode("TARGETED"); // ✅ STORE
|
||||
setQuestionCount(7); // ✅ STORE
|
||||
setDirection(1);
|
||||
setStep("difficulty");
|
||||
}}
|
||||
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 === "difficulty" && (
|
||||
<motion.div
|
||||
key="difficulty"
|
||||
custom={direction}
|
||||
variants={slideVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
className="space-y-4"
|
||||
>
|
||||
<h2 className="text-xl font-satoshi-bold">Select difficulty</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{difficulties.map((d) => (
|
||||
<ChoiceCard
|
||||
key={d}
|
||||
label={d}
|
||||
selected={difficulty === d}
|
||||
onClick={() => {
|
||||
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");
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<p>
|
||||
<strong>Difficulty:</strong> {difficulty}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Duration:</strong> {duration} minutes
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<button
|
||||
disabled={step === "topic"}
|
||||
onClick={() => {
|
||||
const order: Step[] = ["topic", "difficulty", "duration", "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-28 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={() => {
|
||||
handleStartTargetedPractice();
|
||||
}}
|
||||
>
|
||||
Start Test
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user