Skip to content

Commit 95333f0

Browse files
feat: add error handling component and improve URL parameter management in exam and practice pages
1 parent 3c251f7 commit 95333f0

File tree

5 files changed

+288
-37
lines changed

5 files changed

+288
-37
lines changed

app/error.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
import Link from "next/link";
5+
6+
export default function Error({
7+
error,
8+
reset,
9+
}: {
10+
error: Error & { digest?: string };
11+
reset: () => void;
12+
}) {
13+
useEffect(() => {
14+
// Log the error to an error reporting service
15+
console.error("Application error:", error);
16+
}, [error]);
17+
18+
return (
19+
<div className="min-h-screen bg-[var(--color-background)] flex flex-col items-center justify-center px-4">
20+
{/* Header */}
21+
<h1 className="text-2xl font-semibold text-[var(--color-text-primary)] mb-4">
22+
Something went wrong!
23+
</h1>
24+
25+
{/* Description */}
26+
<p className="zoom-area text-[var(--color-text-secondary)] text-lg mb-8 max-w-md text-center">
27+
We&apos;re sorry, but something unexpected happened. Don&apos;t worry,
28+
our <b className="text-[var(--color-primary)]">practice exams</b> are
29+
still here waiting for you!
30+
</p>
31+
32+
{/* Error Details (only in development) */}
33+
{process.env.NODE_ENV === "development" && error.message && (
34+
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg max-w-md w-full">
35+
<p className="text-sm text-red-800 dark:text-red-200 font-mono break-all">
36+
{error.message}
37+
</p>
38+
</div>
39+
)}
40+
41+
{/* Action buttons */}
42+
<div className="flex flex-col sm:flex-row gap-4 items-center">
43+
<button
44+
onClick={reset}
45+
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:opacity-90 transition-opacity font-medium"
46+
>
47+
Try again
48+
</button>
49+
<Link
50+
href="/"
51+
className="px-6 py-3 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg hover:opacity-90 transition-opacity font-medium"
52+
>
53+
📚 Go to Home
54+
</Link>
55+
</div>
56+
57+
{/* Help text */}
58+
<p className="mt-8 text-sm text-[var(--color-text-secondary)] text-center max-w-md">
59+
If this problem persists, please{" "}
60+
<a
61+
href="https://github.com/Ditectrev/Practice-Exams-Platform/issues"
62+
target="_blank"
63+
rel="noopener noreferrer"
64+
className="text-[var(--color-primary)] hover:underline"
65+
>
66+
report it on GitHub
67+
</a>
68+
.
69+
</p>
70+
</div>
71+
);
72+
}

app/exam/page.tsx

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ const questionsQuery = gql`
2727
}
2828
`;
2929

30-
const Exam: NextPage<{ searchParams: { url: string; name: string } }> = ({
31-
searchParams,
32-
}) => {
30+
const Exam: NextPage = () => {
3331
const { isAccessBlocked, isInTrial } = useTrialAccess();
34-
const { url } = searchParams;
32+
const [searchParams, setSearchParams] = useState<URLSearchParams | null>(
33+
null,
34+
);
35+
const url = searchParams?.get("url") || "";
3536
const { minutes, seconds } = {
3637
minutes: 15,
3738
seconds: 0,
@@ -45,16 +46,23 @@ const Exam: NextPage<{ searchParams: { url: string; name: string } }> = ({
4546
const [countAnswered, setCountAnswered] = useState<number>(0);
4647
const { data, loading, error } = useQuery(questionsQuery, {
4748
variables: { range: 30, link: url },
49+
skip: !url, // Skip query if URL is not available
4850
});
4951
const [resultPoints, setResultPoints] = useState<number>(0);
5052
const [passed, setPassed] = useState<boolean>(false);
5153
const [windowWidth, setWindowWidth] = useState<number>(0);
52-
const editedUrl = url.substring(0, url.lastIndexOf("/") + 1);
54+
const editedUrl =
55+
url && url.includes("/") ? url.substring(0, url.lastIndexOf("/") + 1) : "";
5356
const elapsedSeconds =
5457
totalTimeInSeconds -
5558
(parseInt(remainingTime.split(":")[0]) * 60 +
5659
parseInt(remainingTime.split(":")[1]));
5760

61+
useEffect(() => {
62+
const param = new URLSearchParams(window.location.search);
63+
setSearchParams(param);
64+
}, []);
65+
5866
const handleCountAnswered = () => {
5967
setCountAnswered(countAnswered + 1);
6068
};
@@ -89,30 +97,92 @@ const Exam: NextPage<{ searchParams: { url: string; name: string } }> = ({
8997
setCurrentQuestion(data?.randomQuestions[0]);
9098
}, [data]);
9199

92-
// Show loading while checking trial access
93-
if (isAccessBlocked === undefined) {
100+
// Show loading while checking trial access or waiting for URL
101+
if (isAccessBlocked === undefined || !searchParams) {
94102
return <LoadingIndicator />;
95103
}
96104

105+
// Check if URL is missing
106+
if (!url) {
107+
return (
108+
<div className="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4">
109+
<div className="py-10 px-5 mb-6 mx-auto w-[90vw] lg:w-[60vw] 2xl:w-[45%] bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-700 rounded-lg text-center">
110+
<div className="text-red-500 dark:text-red-400 text-lg mb-4">
111+
⚠️ Exam URL is missing. Please select an exam from the home page.
112+
</div>
113+
<button
114+
onClick={() => (window.location.href = "/")}
115+
className="btn-primary text-white px-6 py-2 rounded-lg"
116+
>
117+
Go to Home
118+
</button>
119+
</div>
120+
</div>
121+
);
122+
}
123+
97124
// Block access if trial expired
98125
if (isAccessBlocked) {
99126
return (
100-
<div className="py-10 px-5 mb-6 mx-auto w-[90vw] lg:w-[60vw] 2xl:w-[45%] bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-700 rounded-lg text-center">
101-
<div className="text-red-500 dark:text-red-400 text-lg mb-4">
102-
⏰ Trial expired. Please sign in to continue taking exams.
127+
<div className="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4">
128+
<div className="py-10 px-5 mb-6 mx-auto w-[90vw] lg:w-[60vw] 2xl:w-[45%] bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-700 rounded-lg text-center">
129+
<div className="text-red-500 dark:text-red-400 text-lg mb-4">
130+
⏰ Trial expired. Please sign in to continue taking exams.
131+
</div>
132+
<button
133+
onClick={() => (window.location.href = "/")}
134+
className="btn-primary text-white px-6 py-2 rounded-lg"
135+
>
136+
Go to Home
137+
</button>
103138
</div>
104-
<button
105-
onClick={() => (window.location.href = "/")}
106-
className="btn-primary text-white px-6 py-2 rounded-lg"
107-
>
108-
Go to Home
109-
</button>
110139
</div>
111140
);
112141
}
113142

114143
if (loading) return <LoadingIndicator />;
115-
if (error) return <p>Oh no... {error.message}</p>;
144+
if (error) {
145+
return (
146+
<div className="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4">
147+
<div className="py-10 px-5 mb-6 mx-auto w-[90vw] lg:w-[60vw] 2xl:w-[45%] bg-white dark:bg-gray-800 border-2 border-red-200 dark:border-red-700 rounded-lg text-center">
148+
<div className="text-red-500 dark:text-red-400 text-lg mb-4">
149+
⚠️ Error loading exam questions
150+
</div>
151+
<p className="text-gray-700 dark:text-gray-300 mb-4">
152+
{error.message}
153+
</p>
154+
<button
155+
onClick={() => (window.location.href = "/")}
156+
className="btn-primary text-white px-6 py-2 rounded-lg"
157+
>
158+
Go to Home
159+
</button>
160+
</div>
161+
</div>
162+
);
163+
}
164+
165+
if (!data?.randomQuestions || data.randomQuestions.length === 0) {
166+
return (
167+
<div className="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4">
168+
<div className="py-10 px-5 mb-6 mx-auto w-[90vw] lg:w-[60vw] 2xl:w-[45%] bg-white dark:bg-gray-800 border-2 border-yellow-200 dark:border-yellow-700 rounded-lg text-center">
169+
<div className="text-yellow-600 dark:text-yellow-400 text-lg mb-4">
170+
⚠️ No questions found for this exam
171+
</div>
172+
<p className="text-gray-700 dark:text-gray-300 mb-4">
173+
The exam questions could not be loaded. Please try again later or
174+
select a different exam.
175+
</p>
176+
<button
177+
onClick={() => (window.location.href = "/")}
178+
className="btn-primary text-white px-6 py-2 rounded-lg"
179+
>
180+
Go to Home
181+
</button>
182+
</div>
183+
</div>
184+
);
185+
}
116186

117187
const numberOfQuestions = data.randomQuestions.length || 0;
118188

app/modes/page.tsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,55 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useState, useEffect } from "react";
44
import type { NextPage } from "next";
55
import ExamLink from "@azure-fundamentals/components/ExamLink";
66

7-
const Modes: NextPage<{ searchParams: { url: string; name: string } }> = ({
8-
searchParams,
9-
}) => {
10-
const { url, name } = searchParams;
7+
const Modes: NextPage = () => {
8+
const [searchParams, setSearchParams] = useState<URLSearchParams | null>(
9+
null,
10+
);
11+
const url = searchParams?.get("url") || "";
12+
const name = searchParams?.get("name") || "";
1113
const [hoveredCard, setHoveredCard] = useState<string | null>(null);
1214

15+
useEffect(() => {
16+
const param = new URLSearchParams(window.location.search);
17+
setSearchParams(param);
18+
}, []);
19+
20+
// Show loading while waiting for URL
21+
if (!searchParams) {
22+
return (
23+
<div className="mx-auto mb-6 w-full lg:w-[70vw] 2xl:w-[45%] text-center px-6 pr-8 sm:px-8 sm:pr-12 lg:px-12 lg:pr-16 modes-page">
24+
<div className="text-gray-900 dark:text-gray-100 text-4xl text-leading font-bold uppercase mt-16">
25+
Loading...
26+
</div>
27+
</div>
28+
);
29+
}
30+
31+
// Check if URL or name is missing
32+
if (!url || !name) {
33+
return (
34+
<div className="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4">
35+
<div className="mx-auto w-full lg:w-[70vw] 2xl:w-[45%] text-center px-6 pr-8 sm:px-8 sm:pr-12 lg:px-12 lg:pr-16 modes-page">
36+
<div className="bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-700 rounded-lg py-10 px-5 sm:p-10">
37+
<div className="text-red-500 dark:text-red-400 text-lg mb-4">
38+
⚠️ Exam information is missing. Please select an exam from the
39+
home page.
40+
</div>
41+
<button
42+
onClick={() => (window.location.href = "/")}
43+
className="btn-primary text-white px-6 py-2 rounded-lg"
44+
>
45+
Go to Home
46+
</button>
47+
</div>
48+
</div>
49+
</div>
50+
);
51+
}
52+
1353
return (
1454
<div className="mx-auto mb-6 w-full lg:w-[70vw] 2xl:w-[45%] text-center px-6 pr-8 sm:px-8 sm:pr-12 lg:px-12 lg:pr-16 modes-page">
1555
<h2 className="text-gray-900 dark:text-gray-100 text-4xl text-leading font-bold uppercase mt-16">

0 commit comments

Comments
 (0)