feat(error): add error handling on test screen
This commit is contained in:
@ -18,9 +18,12 @@ import {
|
|||||||
} from "../../components/ui/card";
|
} from "../../components/ui/card";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useExamConfigStore } from "../../stores/useExamConfigStore";
|
||||||
|
|
||||||
export const Practice = () => {
|
export const Practice = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const userXp = useExamConfigStore.getState().userXp;
|
||||||
return (
|
return (
|
||||||
<main className="h-fit max-w-7xl mx-auto px-8 sm:px-6 lg:px-8 py-8 space-y-4">
|
<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">
|
<header className="flex justify-between items-center">
|
||||||
@ -29,7 +32,7 @@ export const Practice = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-100 rounded-full w-fit py-2 px-4 flex items-center gap-2">
|
<div className="bg-purple-100 rounded-full w-fit py-2 px-4 flex items-center gap-2">
|
||||||
<div className="h-2 w-2 bg-linear-to-br from-purple-400 to-purple-500 rounded-full"></div>
|
<div className="h-2 w-2 bg-linear-to-br from-purple-400 to-purple-500 rounded-full"></div>
|
||||||
<span className="font-satoshi-bold text-md">0</span>
|
<span className="font-satoshi-bold text-md">{userXp}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import type { Leaderboard } from "../../types/leaderboard";
|
|||||||
import { api } from "../../utils/api";
|
import { api } from "../../utils/api";
|
||||||
import { Card, CardContent } from "../../components/ui/card";
|
import { Card, CardContent } from "../../components/ui/card";
|
||||||
import { LeaderboardRowSkeleton } from "../../components/LeaderboardSkeleton";
|
import { LeaderboardRowSkeleton } from "../../components/LeaderboardSkeleton";
|
||||||
|
import { useExamConfigStore } from "../../stores/useExamConfigStore";
|
||||||
|
|
||||||
export const Rewards = () => {
|
export const Rewards = () => {
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
@ -45,6 +46,8 @@ export const Rewards = () => {
|
|||||||
const [leaderboard, setLeaderboard] = useState<Leaderboard>();
|
const [leaderboard, setLeaderboard] = useState<Leaderboard>();
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { setUserXp } = useExamConfigStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLeaderboard = async () => {
|
const fetchLeaderboard = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@ -64,6 +67,7 @@ export const Rewards = () => {
|
|||||||
const response = await api.fetchLeaderboard(token);
|
const response = await api.fetchLeaderboard(token);
|
||||||
|
|
||||||
setLeaderboard(response);
|
setLeaderboard(response);
|
||||||
|
setUserXp(response.user_rank.total_xp);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "../../../components/ui/card";
|
} from "../../../components/ui/card";
|
||||||
import { Check, Loader2 } from "lucide-react";
|
import { Check, Loader2, Unplug } from "lucide-react";
|
||||||
|
|
||||||
import { api } from "../../../utils/api";
|
import { api } from "../../../utils/api";
|
||||||
import { useAuthStore } from "../../../stores/authStore";
|
import { useAuthStore } from "../../../stores/authStore";
|
||||||
@ -46,6 +46,7 @@ export const Test = () => {
|
|||||||
const [eliminated, setEliminated] = useState<Record<string, Set<string>>>({});
|
const [eliminated, setEliminated] = useState<Record<string, Set<string>>>({});
|
||||||
|
|
||||||
const [showExitDialog, setShowExitDialog] = useState(false);
|
const [showExitDialog, setShowExitDialog] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (blocker.state === "blocked") {
|
if (blocker.state === "blocked") {
|
||||||
@ -94,6 +95,7 @@ export const Test = () => {
|
|||||||
// ✅ NOW start module phase
|
// ✅ NOW start module phase
|
||||||
useSatExam.getState().startExam();
|
useSatExam.getState().startExam();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setError(`Failed to start exam session: ${error}`);
|
||||||
console.error("Failed to start exam session:", error);
|
console.error("Failed to start exam session:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -308,6 +310,36 @@ export const Test = () => {
|
|||||||
|
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case "IDLE":
|
case "IDLE":
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen px-8 py-8 w-full space-y-6 flex flex-col justify-center">
|
||||||
|
<Card className="p-4 bg-red-100 ring-1 ring-red-500 w-full">
|
||||||
|
<CardContent className="flex items-center justify-between w-full gap-4">
|
||||||
|
<Unplug size={80} color="red" />
|
||||||
|
|
||||||
|
<span className="font-satoshi-bold text-red-500">{error}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
useExamConfigStore.getState().clearPayload();
|
||||||
|
navigate(`/student/home`);
|
||||||
|
}}
|
||||||
|
className="font-satoshi rounded-3xl text-lg w-full py-4 bg-linear-to-br from-slate-500 to-slate-600 text-white active:bg-linear-to-br active:from-slate-600 active:to-slate-700 "
|
||||||
|
>
|
||||||
|
Go Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => startExam()}
|
||||||
|
className="font-satoshi rounded-3xl text-lg w-full py-4 bg-linear-to-br from-purple-500 to-purple-600 text-white active:bg-linear-to-br active:from-purple-600 active:to-purple-700 "
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen px-8 py-8 w-full space-y-6 flex flex-col items-center justify-center">
|
<main className="min-h-screen px-8 py-8 w-full space-y-6 flex flex-col items-center justify-center">
|
||||||
<Card className="">
|
<Card className="">
|
||||||
@ -315,46 +347,6 @@ export const Test = () => {
|
|||||||
<CardTitle className="font-satoshi text-4xl">
|
<CardTitle className="font-satoshi text-4xl">
|
||||||
Ready to begin your test?
|
Ready to begin your test?
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{/* <CardDescription>
|
|
||||||
<section className="flex justify-between gap-6 px-4">
|
|
||||||
<div className="flex flex-col justify-center items-center gap-4">
|
|
||||||
<div className="w-fit bg-cyan-100 p-2 rounded-full">
|
|
||||||
<Clock size={30} color="oklch(60.9% 0.126 221.723)" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-center items-center">
|
|
||||||
<h3 className="text-xl font-satoshi-bold text-black">
|
|
||||||
{practiceSheet?.time_limit}
|
|
||||||
</h3>
|
|
||||||
<p className="text-md font-satoshi ">Minutes</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-center items-center gap-4">
|
|
||||||
<div className="w-fit bg-lime-100 p-2 rounded-full">
|
|
||||||
<CircleQuestionMark
|
|
||||||
size={30}
|
|
||||||
color="oklch(64.8% 0.2 131.684)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-center items-center">
|
|
||||||
<h3 className="text-xl font-satoshi-bold text-black">
|
|
||||||
{practiceSheet?.questions_count}
|
|
||||||
</h3>
|
|
||||||
<p className="text-md font-satoshi ">Questions</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-center items-center gap-4">
|
|
||||||
<div className="w-fit bg-amber-100 p-2 rounded-full">
|
|
||||||
<Layers size={30} color="oklch(66.6% 0.179 58.318)" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-center items-center">
|
|
||||||
<h3 className="text-xl font-satoshi-bold text-black">
|
|
||||||
{practiceSheet?.modules.length}
|
|
||||||
</h3>
|
|
||||||
<p className="text-md font-satoshi ">Modules</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</CardDescription> */}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<h2 className="font-satoshi-bold text-2xl">Before you begin:</h2>
|
<h2 className="font-satoshi-bold text-2xl">Before you begin:</h2>
|
||||||
@ -377,12 +369,7 @@ export const Test = () => {
|
|||||||
Your progress will be saved automatically
|
Your progress will be saved automatically
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Check size={24} color="oklch(62.7% 0.265 303.9)" />
|
|
||||||
<span className="font-satoshi">
|
|
||||||
You can take breaks using the "More" menu in the top right
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => startExam()}
|
onClick={() => startExam()}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { StartExamPayload, ExamMode } from "../types/test";
|
|||||||
|
|
||||||
interface ExamConfigState {
|
interface ExamConfigState {
|
||||||
payload: StartExamPayload | null;
|
payload: StartExamPayload | null;
|
||||||
|
userXp: number;
|
||||||
|
|
||||||
setSheetId: (id: string) => void;
|
setSheetId: (id: string) => void;
|
||||||
storeTopics: (ids: string[]) => void;
|
storeTopics: (ids: string[]) => void;
|
||||||
@ -14,6 +15,7 @@ interface ExamConfigState {
|
|||||||
storeDuration: (minutes: number) => void;
|
storeDuration: (minutes: number) => void;
|
||||||
setMode: (mode: ExamMode) => void;
|
setMode: (mode: ExamMode) => void;
|
||||||
setSection: (section: string) => void;
|
setSection: (section: string) => void;
|
||||||
|
setUserXp: (section: number) => void;
|
||||||
|
|
||||||
clearPayload: () => void;
|
clearPayload: () => void;
|
||||||
}
|
}
|
||||||
@ -22,6 +24,7 @@ export const useExamConfigStore = create<ExamConfigState>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
payload: null,
|
payload: null,
|
||||||
|
userXp: 0,
|
||||||
|
|
||||||
setSheetId: (sheet_id) =>
|
setSheetId: (sheet_id) =>
|
||||||
set({
|
set({
|
||||||
@ -45,6 +48,13 @@ export const useExamConfigStore = create<ExamConfigState>()(
|
|||||||
section,
|
section,
|
||||||
} as StartExamPayload,
|
} as StartExamPayload,
|
||||||
}),
|
}),
|
||||||
|
setUserXp: (userXp) =>
|
||||||
|
set({
|
||||||
|
payload: {
|
||||||
|
...(get().payload ?? {}),
|
||||||
|
userXp,
|
||||||
|
} as StartExamPayload,
|
||||||
|
}),
|
||||||
|
|
||||||
setDifficulty: (difficulty) =>
|
setDifficulty: (difficulty) =>
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in New Issue
Block a user