Skip to content

Commit 8c53a22

Browse files
Merge pull request #82 from codersforcauses/issue-46-Add_custom_404_page
Issue 46 add custom 404 page
2 parents d53bf10 + 2f86a27 commit 8c53a22

2 files changed

Lines changed: 603 additions & 0 deletions

File tree

client/src/pages/404.tsx

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useEffect, useState } from "react";
5+
6+
import { Button } from "@/components/ui/button";
7+
import TRIVIA from "@/trivia.json";
8+
9+
interface Trivia {
10+
question: string;
11+
answer: string;
12+
}
13+
14+
export default function Custom404() {
15+
const [gameQuestions, setGameQuestions] = useState<Trivia[]>([]);
16+
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
17+
const [score, setScore] = useState(0);
18+
const [gameActive, setGameActive] = useState(false);
19+
const [timeLeft, setTimeLeft] = useState(30);
20+
const [answered, setAnswered] = useState(false);
21+
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null);
22+
const [options, setOptions] = useState<string[]>([]);
23+
24+
const currentTrivia = gameQuestions[currentQuestionIndex];
25+
26+
const generateQuestions = () => {
27+
const shuffled = TRIVIA.sort(() => Math.random() - 0.5).slice(0, 10);
28+
setGameQuestions(shuffled);
29+
};
30+
31+
useEffect(() => {
32+
if (!currentTrivia) return;
33+
34+
const wrongAnswers = TRIVIA.filter((t) => t.answer !== currentTrivia.answer)
35+
.sort(() => Math.random() - 0.5)
36+
.slice(0, 3)
37+
.map((t) => t.answer);
38+
39+
const allOptions = [currentTrivia.answer, ...wrongAnswers].sort(
40+
() => Math.random() - 0.5,
41+
);
42+
setOptions(allOptions);
43+
}, [currentQuestionIndex, currentTrivia]);
44+
45+
useEffect(() => {
46+
if (!gameActive || timeLeft <= 0) return;
47+
48+
const timer = setInterval(() => {
49+
setTimeLeft((prev) => {
50+
if (prev <= 1) {
51+
setGameActive(false);
52+
return 0;
53+
}
54+
return prev - 1;
55+
});
56+
}, 1000);
57+
58+
return () => clearInterval(timer);
59+
}, [gameActive, timeLeft]);
60+
61+
const startGame = () => {
62+
generateQuestions();
63+
setGameActive(true);
64+
setCurrentQuestionIndex(0);
65+
setScore(0);
66+
setTimeLeft(30);
67+
setAnswered(false);
68+
setSelectedAnswer(null);
69+
};
70+
71+
const handleAnswer = (answer: string) => {
72+
if (answered) return;
73+
74+
setSelectedAnswer(answer);
75+
setAnswered(true);
76+
77+
if (answer === currentTrivia.answer) {
78+
setScore((prev) => prev + 1);
79+
}
80+
81+
setTimeout(() => {
82+
if (currentQuestionIndex < gameQuestions.length - 1) {
83+
setCurrentQuestionIndex((prev) => prev + 1);
84+
setAnswered(false);
85+
setSelectedAnswer(null);
86+
} else {
87+
setGameActive(false);
88+
}
89+
}, 1000);
90+
};
91+
92+
const getButtonClass = (option: string) => {
93+
const baseClass =
94+
"w-full rounded border p-2.5 text-left text-sm transition-all cursor-pointer md:p-3 md:text-base";
95+
96+
if (!answered) {
97+
return `${baseClass} bg-card border-border text-foreground hover:border-accent`;
98+
}
99+
100+
if (option === currentTrivia.answer) {
101+
return `${baseClass} bg-primary border-accent text-accent-foreground font-semibold`;
102+
}
103+
104+
if (option === selectedAnswer && option !== currentTrivia.answer) {
105+
return `${baseClass} bg-accent border-secondary text-secondary-foreground`;
106+
}
107+
108+
return `${baseClass} bg-muted border-border text-muted-foreground`;
109+
};
110+
111+
return (
112+
<main className="mx-auto flex h-dvh max-w-6xl items-center overflow-hidden px-4 py-4 sm:px-6 sm:py-6 md:px-16 md:py-8 lg:px-20 lg:py-10">
113+
<div className="mx-auto w-full max-w-2xl">
114+
<h1 className="mb-4 text-center font-jersey10 text-5xl text-primary md:mb-5 md:text-[3.25rem]">
115+
404
116+
</h1>
117+
<h2 className="mb-2 text-center font-jersey10 text-xl text-foreground md:mb-3 md:text-[1.4rem]">
118+
Page Not Found
119+
</h2>
120+
121+
<p className="mb-4 text-center text-sm text-foreground md:mb-5 md:text-base">
122+
Test your game knowledge with some rapid-fire trivia instead!!!
123+
</p>
124+
125+
{!gameActive ? (
126+
<div className="mb-4 space-y-3 md:mb-5 md:space-y-3.5">
127+
{gameQuestions.length === 0 ? (
128+
<>
129+
<p className="mb-3 text-sm text-muted-foreground md:mb-3 md:text-base">
130+
Answer 10 random gaming trivia questions in 30 seconds!
131+
</p>
132+
<Button onClick={startGame} className="w-full">
133+
Start Trivia
134+
</Button>
135+
</>
136+
) : (
137+
<>
138+
<p className="mb-3 text-center font-jersey10 text-2xl font-bold text-primary md:mb-3">
139+
Game Over!
140+
</p>
141+
<p className="mb-3 text-center text-sm text-foreground md:mb-3 md:text-base">
142+
Final Score:
143+
<span className="text-xl font-bold text-primary">
144+
{score}
145+
</span>{" "}
146+
/ 10
147+
</p>
148+
<Button onClick={startGame} className="w-full">
149+
Play Again
150+
</Button>
151+
</>
152+
)}
153+
</div>
154+
) : (
155+
<div className="md:space-y-4.5 mb-4 space-y-4 md:mb-5">
156+
<div className="mb-2 flex items-center justify-between md:mb-3">
157+
<p className="text-xs text-muted-foreground md:text-sm">
158+
Question {currentQuestionIndex + 1} / 10
159+
</p>
160+
<div
161+
className={`font-jersey10 text-2xl font-bold md:text-[1.9rem] ${
162+
timeLeft <= 10 ? "text-destructive" : "text-primary"
163+
}`}
164+
>
165+
{timeLeft}s
166+
</div>
167+
</div>
168+
169+
<div className="md:space-y-4.5 space-y-4 rounded border border-border bg-card p-4 md:p-5">
170+
<p className="text-base leading-snug text-foreground md:text-[1.05rem]">
171+
{currentTrivia.question}
172+
</p>
173+
174+
<div className="space-y-2.5 md:space-y-3">
175+
{options.map((option, idx) => (
176+
<button
177+
key={idx}
178+
onClick={() => handleAnswer(option)}
179+
className={getButtonClass(option)}
180+
disabled={answered}
181+
>
182+
{option}
183+
</button>
184+
))}
185+
</div>
186+
187+
<p className="text-xs text-foreground md:text-sm">
188+
Score:{" "}
189+
<span className="font-jersey10 font-bold text-primary">
190+
{score} / 10
191+
</span>
192+
</p>
193+
</div>
194+
</div>
195+
)}
196+
197+
<Link href="/">
198+
<Button variant="outline" className="w-full">
199+
Go Home
200+
</Button>
201+
</Link>
202+
</div>
203+
</main>
204+
);
205+
}

0 commit comments

Comments
 (0)