Skip to content

Commit d9c82f6

Browse files
author
Callum McIntyre
committed
Add implementation of quiz w/o any reward mechanic
1 parent 6c25403 commit d9c82f6

18 files changed

+507
-291
lines changed

components/footer.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default function Footer() {
2+
return (
3+
<footer className="mx-auto mt-48 text-center">
4+
<a
5+
href="https://www.pointer.gg?utm_source=thirdweb-lootbox"
6+
target="_blank"
7+
rel="noopener noreferrer"
8+
>
9+
Learn web3 dev and earn crypto rewards at{" "}
10+
<span className="underline">Pointer</span>
11+
</a>
12+
</footer>
13+
);
14+
}

components/layout.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from "react";
2+
import Footer from "./footer";
3+
import Navbar from "./navbar";
4+
5+
type Props = {
6+
title: string;
7+
};
8+
9+
export default function Layout({
10+
children,
11+
title,
12+
}: React.PropsWithChildren<Props>) {
13+
return (
14+
<>
15+
<Navbar />
16+
<main className="max-w-3xl mx-auto my-4">
17+
<div className="flex flex-col gap-8">
18+
<h1 className="text-6xl font-bold text-blue-600">{title}</h1>
19+
{children}
20+
</div>
21+
</main>
22+
<Footer />
23+
</>
24+
);
25+
}

components/navbar.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Link from "next/link";
2+
3+
export default function Navbar() {
4+
return (
5+
<nav className="font-sans flex justify-between pt-4 pb-20 px-4 md:px-20 w-full h-10 ">
6+
<div className="flex gap-4 md:gap-10">
7+
<Link href="/">
8+
<a className="text-2xl no-underline text-grey-darkest hover:text-blue-900">
9+
Quiz
10+
</a>
11+
</Link>
12+
<Link href="/lounge">
13+
<a className="text-2xl no-underline text-grey-darkest hover:text-blue-900">
14+
Winner's Lounge
15+
</a>
16+
</Link>
17+
</div>
18+
</nav>
19+
);
20+
}

components/primary-button.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ButtonHTMLAttributes } from "react";
2+
3+
export default function PrimaryButton(
4+
props: ButtonHTMLAttributes<HTMLButtonElement>
5+
) {
6+
const { children, ...rest } = props;
7+
const className =
8+
"max-w-fit inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-80 disabled:pointer-events-none";
9+
10+
return (
11+
<button {...rest} className={className}>
12+
{children}
13+
</button>
14+
);
15+
}

components/quiz-game.tsx

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useState } from "react";
2+
import { Question } from "../lib/questions";
3+
import QuizQuestion from "./quiz-question";
4+
5+
export type QuestionWithoutAnswer = Omit<Question, "correctAnswerIndex">;
6+
7+
type Props = {
8+
questions: QuestionWithoutAnswer[];
9+
};
10+
11+
export default function QuizGame({ questions }: Props) {
12+
const [questionIndex, setQuestionIndex] = useState(0);
13+
const [quizComplete, setQuizComplete] = useState(false);
14+
15+
const nextQuestion = () => {
16+
if (questionIndex + 1 < questions.length) {
17+
setQuestionIndex(questionIndex + 1);
18+
} else {
19+
setQuizComplete(true);
20+
}
21+
};
22+
23+
if (quizComplete) {
24+
return <p>You&apos;ve reached the end of the quiz!</p>;
25+
}
26+
27+
const question = questions[questionIndex];
28+
29+
return (
30+
<QuizQuestion
31+
key={questionIndex}
32+
questionIndex={questionIndex}
33+
questionText={question.questionText}
34+
image={question.image}
35+
answers={question.answers}
36+
nextQuestionFunction={nextQuestion}
37+
/>
38+
);
39+
}

components/quiz-question.tsx

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import axios from "axios";
2+
import Link from "next/link";
3+
import { FormEvent, useState } from "react";
4+
import {
5+
CheckAnswerPayload,
6+
CheckAnswerResponse,
7+
} from "../pages/api/check-answer";
8+
import PrimaryButton from "./primary-button";
9+
import invariant from "tiny-invariant";
10+
11+
type Props = {
12+
questionIndex: number;
13+
questionText: string;
14+
image?: string;
15+
answers: string[];
16+
nextQuestionFunction: () => void;
17+
};
18+
19+
type AnswerResult = "correct" | "incorrect";
20+
21+
export default function QuizQuestion({
22+
questionIndex,
23+
questionText,
24+
image,
25+
answers,
26+
nextQuestionFunction,
27+
}: Props) {
28+
const [answerIndex, setAnswerIndex] = useState<number | undefined>(undefined);
29+
const [submitting, setSubmitting] = useState(false);
30+
const [error, setError] = useState<string | undefined>(undefined);
31+
const [answerResult, setAnswerResult] = useState<AnswerResult | undefined>(
32+
undefined
33+
);
34+
const [correctAnswerWas, setCorrectAnswerWas] = useState<number | undefined>(
35+
undefined
36+
);
37+
38+
const handleSubmit = async (e: FormEvent) => {
39+
e.preventDefault();
40+
setSubmitting(true);
41+
42+
try {
43+
invariant(
44+
answerIndex !== undefined,
45+
"Answer index is required to submit"
46+
);
47+
48+
const payload: CheckAnswerPayload = {
49+
questionIndex,
50+
answerIndex,
51+
};
52+
53+
const checkResponse = await axios.post("/api/check-answer", payload);
54+
const result = checkResponse.data as CheckAnswerResponse;
55+
56+
if (result.kind === "error") {
57+
setError(result.error);
58+
}
59+
60+
if (result.kind === "correct") {
61+
setAnswerResult("correct");
62+
setCorrectAnswerWas(answerIndex);
63+
}
64+
65+
if (result.kind === "incorrect") {
66+
setAnswerResult("incorrect");
67+
setCorrectAnswerWas(result.correctAnswerIndex);
68+
}
69+
} finally {
70+
setSubmitting(false);
71+
}
72+
};
73+
74+
const renderResult = () => {
75+
if (submitting) {
76+
return <PrimaryButton disabled={true}>Checking Answer...</PrimaryButton>;
77+
}
78+
79+
if (answerResult === "correct") {
80+
return (
81+
<>
82+
<p className="text-green-800">
83+
Congratulations! That was the right answer!
84+
</p>
85+
<p>
86+
A pack will be sent to you shortly. You'll be able to check it out
87+
and open it in the{" "}
88+
<Link href="/lounge">
89+
<a className="underline hover:no-underline">lounge</a>
90+
</Link>
91+
!
92+
</p>
93+
</>
94+
);
95+
}
96+
97+
if (answerResult === "incorrect") {
98+
return <p className="text-red-800">Sorry, that was incorrect!</p>;
99+
}
100+
101+
return (
102+
<>
103+
<PrimaryButton
104+
type="submit"
105+
onClick={handleSubmit}
106+
disabled={answerIndex === undefined}
107+
>
108+
Check Answer
109+
</PrimaryButton>
110+
{error ? <p className="text-red-500">{error}</p> : null}
111+
</>
112+
);
113+
};
114+
115+
return (
116+
<form>
117+
<div className="flex flex-col gap-4">
118+
<div>
119+
<div className="flex flex-col gap-2">
120+
<label className="font-medium text-lg text-gray-900">
121+
{questionText}
122+
</label>
123+
{image ? (
124+
<img src={image} className="object-cover h-48 w-96" alt="" />
125+
) : null}
126+
</div>
127+
<fieldset className="mt-4">
128+
<div className="space-y-4">
129+
{answers.map((answerText, i) => (
130+
<div key={i} className="flex items-center">
131+
<input
132+
id={i.toString()}
133+
name="quiz-answer"
134+
type="radio"
135+
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 peer disabled:cursor-not-allowed"
136+
value={i}
137+
checked={answerIndex === i}
138+
onChange={(e) => setAnswerIndex(Number(e.target.value))}
139+
disabled={answerResult !== undefined}
140+
/>
141+
<label
142+
htmlFor={i.toString()}
143+
className="ml-3 block text-sm font-medium text-gray-700 peer-disabled:text-gray-500 peer-disabled:cursor-not-allowed"
144+
>
145+
{answerText}
146+
{i === correctAnswerWas ? <span></span> : null}
147+
</label>
148+
</div>
149+
))}
150+
</div>
151+
</fieldset>
152+
</div>
153+
154+
{renderResult()}
155+
156+
{answerResult !== undefined ? (
157+
<PrimaryButton onClick={nextQuestionFunction}>
158+
Next Question
159+
</PrimaryButton>
160+
) : null}
161+
</div>
162+
</form>
163+
);
164+
}

lib/questions.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export type Question = {
2+
questionText: string;
3+
image?: string;
4+
answers: string[];
5+
correctAnswerIndex?: number;
6+
};
7+
8+
const quizQuestions: Question[] = [
9+
{
10+
questionText: "What do the initials DB in Aston Martin DB11 stand for?",
11+
image:
12+
"https://images.unsplash.com/photo-1642201855395-1c8b44e6e42b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=880&q=80",
13+
answers: [
14+
"Trick question: nothing!",
15+
"David Brown",
16+
"Drive Better",
17+
"Diane Blue",
18+
],
19+
correctAnswerIndex: 1,
20+
},
21+
{
22+
questionText: "Which car brand is this logo for?",
23+
image: "https://www.carlogos.org/logo/Lexus-symbol-640x480.jpg",
24+
answers: ["Lamborghini", "Lada", "Lotus", "Lexus"],
25+
correctAnswerIndex: 3,
26+
},
27+
{
28+
questionText: "Where in the UK is the MINI plant?",
29+
image:
30+
"https://images.unsplash.com/photo-1591439346018-9d5df732ab7d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1471&q=80",
31+
answers: ["Oxford", "Cambridge", "London", "Edinburgh"],
32+
correctAnswerIndex: 0,
33+
},
34+
{
35+
questionText:
36+
"Which was the first James Bond film to include an Aston Martin?",
37+
answers: ["Dr No", "From Russia with Love", "Goldfinger", "Thunderball"],
38+
correctAnswerIndex: 2,
39+
},
40+
{
41+
questionText: "What color were all Ferraris originally?",
42+
answers: ["Yellow", "White", "Blue", "Red"],
43+
correctAnswerIndex: 3,
44+
},
45+
];
46+
47+
export default quizQuestions;

next.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ module.exports = {
44
experimental: {
55
outputStandalone: true,
66
},
7-
}
7+
};

0 commit comments

Comments
 (0)