Skip to content

Commit c1d1dea

Browse files
authored
Merge pull request #172 from AlanRacciatti/feature_quizzes
feature: quizzes
2 parents 57443a4 + c84c379 commit c1d1dea

File tree

5 files changed

+508
-0
lines changed

5 files changed

+508
-0
lines changed

components/mdx/Components.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
33
import dracula from 'react-syntax-highlighter/dist/cjs/styles/prism/dracula'
44
import { CopyToClipboard } from '../CopyToClipboard'
55
import SideDrawer from './SideDrawer'
6+
import Quiz from './Quiz'
7+
import Question from './Question'
68
import Callout from './Callout'
79

810
const Components = {
@@ -41,6 +43,8 @@ const Components = {
4143
),
4244
SideDrawer,
4345
Callout,
46+
Quiz,
47+
Question,
4448
}
4549

4650
export default Components

components/mdx/Question.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Box, VStack, Text, Button, useToast } from '@chakra-ui/react'
2+
import React, { FC, useState } from 'react'
3+
4+
interface QuestionProps {
5+
question: string
6+
}
7+
8+
interface Question {
9+
question: string
10+
options: [
11+
{
12+
answer: string
13+
correct?: boolean
14+
},
15+
]
16+
}
17+
18+
const Question: FC<QuestionProps> = (props: QuestionProps) => {
19+
const question: Question = require(`../../utils/questions/${props.question}.json`)
20+
const [optionSelected, setOptionSelected]: [
21+
number,
22+
React.Dispatch<React.SetStateAction<number>>,
23+
] = useState(-1)
24+
const [answersSubmitted, setAnswersSubmitted] = useState(false)
25+
const toast = useToast()
26+
27+
const selectAnswer = (optionIndex: number) => {
28+
setOptionSelected(optionIndex)
29+
}
30+
31+
const getOptionBackground = (optionIndex: number) => {
32+
if (optionSelected == optionIndex) {
33+
return 'yellow.600'
34+
}
35+
return 'gray.600'
36+
}
37+
38+
const quizNotAnswered = () => {
39+
toast({
40+
title: 'No answer selected',
41+
description: 'Choose an answer!',
42+
status: 'warning',
43+
duration: 9000,
44+
isClosable: true,
45+
})
46+
}
47+
48+
const quizFailedToast = () => {
49+
toast({
50+
title: 'Wrong answer',
51+
description: `Try again fren`,
52+
status: 'error',
53+
duration: 9000,
54+
isClosable: true,
55+
})
56+
}
57+
58+
const quizSuccessToast = () => {
59+
toast({
60+
title: 'Correct answer!',
61+
description: "Let's goooo",
62+
status: 'success',
63+
duration: 9000,
64+
isClosable: true,
65+
})
66+
}
67+
68+
const submit = () => {
69+
if (optionSelected === -1) {
70+
return quizNotAnswered()
71+
}
72+
73+
if (question.options[optionSelected].correct) {
74+
return quizSuccessToast()
75+
}
76+
77+
return quizFailedToast()
78+
}
79+
80+
return (
81+
<VStack
82+
spacing={4}
83+
background="gray.900"
84+
padding="6"
85+
borderRadius="md"
86+
mt="7"
87+
>
88+
<Text fontWeight="bold" textAlign="left" w="100%">
89+
{question.question}
90+
</Text>
91+
{question.options.map((o, index) => {
92+
return (
93+
<Box
94+
w="100%"
95+
borderRadius="md"
96+
background={getOptionBackground(index)}
97+
padding="3"
98+
cursor="pointer"
99+
onClick={() => selectAnswer(index)}
100+
key={index}
101+
>
102+
{o.answer}
103+
</Box>
104+
)
105+
})}
106+
<Button
107+
mx="1"
108+
mt="7"
109+
colorScheme="green"
110+
backgroundColor="green.400"
111+
onClick={submit}
112+
alignSelf="flex-start"
113+
>
114+
Submit
115+
</Button>
116+
</VStack>
117+
)
118+
}
119+
120+
export default Question

components/mdx/Quiz.tsx

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import {
2+
Box,
3+
VStack,
4+
Text,
5+
Button,
6+
Modal,
7+
ModalOverlay,
8+
ModalContent,
9+
ModalHeader,
10+
ModalFooter,
11+
ModalBody,
12+
ModalCloseButton,
13+
useToast,
14+
} from '@chakra-ui/react'
15+
import React, { FC, useState } from 'react'
16+
17+
interface QuizProps {
18+
quiz: string
19+
}
20+
21+
interface Quiz {
22+
title: string
23+
questions: [
24+
{
25+
question: string
26+
options: [
27+
{
28+
answer: string
29+
correct?: boolean
30+
},
31+
]
32+
},
33+
]
34+
}
35+
36+
interface Answers {
37+
[index: string]: number
38+
}
39+
40+
const Quiz: FC<QuizProps> = (props: QuizProps) => {
41+
const quiz: Quiz = require(`../../utils/quizzes/${props.quiz}.json`)
42+
const [showQuiz, setShowQuiz] = useState(false)
43+
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
44+
const [answers, setAnswers] = useState<Answers>({})
45+
const [correctAnswers, setCorrectAnswers] = useState<number[] | null>(null)
46+
const toast = useToast()
47+
48+
const nextQuestion = () => {
49+
setCurrentQuestionIndex(currentQuestionIndex + 1)
50+
}
51+
52+
const previousQuestion = () => {
53+
setCurrentQuestionIndex(currentQuestionIndex - 1)
54+
}
55+
56+
const previousButtonVisibility = () => {
57+
return currentQuestionIndex == 0 ? 'hidden' : 'visible'
58+
}
59+
60+
const nextButtonVisibility = () => {
61+
return currentQuestionIndex + 1 == quiz.questions.length
62+
? 'hidden'
63+
: 'visible'
64+
}
65+
66+
const selectAnswer = (answerIndex: number) => {
67+
let newAnswers: Answers = { ...answers }
68+
newAnswers[currentQuestionIndex.toString()] = answerIndex
69+
setAnswers(newAnswers)
70+
}
71+
72+
const getQuestionBackground = (optionIndex: number) => {
73+
if (
74+
correctAnswers &&
75+
correctAnswers.indexOf(currentQuestionIndex) !== -1 &&
76+
quiz.questions[currentQuestionIndex].options[optionIndex].correct &&
77+
answers[currentQuestionIndex] === optionIndex
78+
) {
79+
return 'green.500'
80+
}
81+
82+
if (answers[currentQuestionIndex] == optionIndex) {
83+
return 'yellow.600'
84+
}
85+
return 'gray.600'
86+
}
87+
88+
const quizNotAnswered = () => {
89+
toast({
90+
title: '!answers',
91+
description: 'Please answer all the questions',
92+
status: 'warning',
93+
duration: 9000,
94+
isClosable: true,
95+
})
96+
}
97+
98+
const quizFailedToast = (wrongAnswersCounter: number) => {
99+
toast({
100+
title: 'Quiz failed',
101+
description: `You have ${wrongAnswersCounter} wrong answers :(`,
102+
status: 'error',
103+
duration: 9000,
104+
isClosable: true,
105+
})
106+
}
107+
108+
const quizSuccessToast = () => {
109+
toast({
110+
title: 'Amazing!',
111+
description: 'You have passed the lesson!',
112+
status: 'success',
113+
duration: 9000,
114+
isClosable: true,
115+
})
116+
}
117+
118+
const submit = () => {
119+
if (quiz.questions.length != Object.keys(answers).length) {
120+
return quizNotAnswered()
121+
}
122+
123+
let hasWrongAnswers = false
124+
let wrongAnswersCounter = 0
125+
126+
const newCorrectAnswers: number[] = []
127+
128+
quiz.questions.forEach((q, index) => {
129+
if (!q.options[answers[index]].correct) {
130+
hasWrongAnswers = true
131+
wrongAnswersCounter++
132+
} else {
133+
newCorrectAnswers.push(index)
134+
}
135+
})
136+
137+
setCorrectAnswers(newCorrectAnswers)
138+
139+
if (hasWrongAnswers) {
140+
return quizFailedToast(wrongAnswersCounter)
141+
}
142+
143+
return quizSuccessToast()
144+
}
145+
146+
const cancelQuiz = () => {
147+
setAnswers({})
148+
setShowQuiz(false)
149+
setCurrentQuestionIndex(0)
150+
}
151+
152+
return (
153+
<>
154+
<Button
155+
colorScheme="yellow"
156+
backgroundColor="yellow.600"
157+
color="white"
158+
display="flex"
159+
margin="auto"
160+
onClick={() => setShowQuiz(true)}
161+
>
162+
Take quiz
163+
</Button>
164+
165+
<Modal closeOnOverlayClick={false} isOpen={showQuiz} onClose={cancelQuiz}>
166+
<ModalOverlay />
167+
<ModalContent>
168+
<ModalHeader>{quiz.title}</ModalHeader>
169+
<ModalCloseButton />
170+
<ModalBody pb={6}>
171+
<VStack
172+
spacing={4}
173+
background="gray.900"
174+
padding="6"
175+
borderRadius="md"
176+
>
177+
<Text fontWeight="bold" w="100%">
178+
{quiz.questions[currentQuestionIndex].question}
179+
</Text>
180+
{quiz.questions[currentQuestionIndex].options.map((o, index) => {
181+
return (
182+
<Box
183+
w="100%"
184+
borderRadius="md"
185+
background={getQuestionBackground(index)}
186+
padding="3"
187+
cursor="pointer"
188+
onClick={() => selectAnswer(index)}
189+
key={index}
190+
>
191+
{o.answer}
192+
</Box>
193+
)
194+
})}
195+
<Box
196+
display="flex"
197+
justifyContent="space-between"
198+
w="100%"
199+
alignItems="center"
200+
>
201+
<Text w="100%">{`Question ${currentQuestionIndex + 1}/${
202+
quiz.questions.length
203+
}`}</Text>
204+
<Box w="100%" display="flex">
205+
<Button
206+
mx="2"
207+
visibility={previousButtonVisibility()}
208+
onClick={previousQuestion}
209+
>
210+
{'< Previous'}
211+
</Button>
212+
<Button
213+
visibility={nextButtonVisibility()}
214+
onClick={nextQuestion}
215+
>
216+
{'Next >'}
217+
</Button>
218+
</Box>
219+
</Box>
220+
</VStack>
221+
</ModalBody>
222+
223+
<ModalFooter>
224+
<Button
225+
mx="1"
226+
colorScheme="red"
227+
backgroundColor="red.600"
228+
onClick={cancelQuiz}
229+
>
230+
Cancel
231+
</Button>
232+
<Button
233+
mx="1"
234+
colorScheme="green"
235+
backgroundColor="green.400"
236+
onClick={submit}
237+
>
238+
Submit
239+
</Button>
240+
</ModalFooter>
241+
</ModalContent>
242+
</Modal>
243+
</>
244+
)
245+
}
246+
247+
export default Quiz

0 commit comments

Comments
 (0)