Skip to content

Commit 7c7ab37

Browse files
committed
Finished course section 16
1 parent b89640a commit 7c7ab37

File tree

9 files changed

+214
-43
lines changed

9 files changed

+214
-43
lines changed

public/index.html

+19-21
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
<!DOCTYPE html>
22
<html lang="en">
3-
<head>
4-
<meta charset="utf-8" />
5-
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1" />
7-
<meta name="theme-color" content="#000000" />
8-
<meta
9-
name="description"
10-
content="Web site created using create-react-app"
11-
/>
12-
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
13-
<!--
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
<meta
9+
name="description"
10+
content="Web site created using create-react-app" />
11+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
12+
<!--
1413
manifest.json provides metadata used when your web app is installed on a
1514
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
1615
-->
17-
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
18-
<!--
16+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
17+
<!--
1918
Notice the use of %PUBLIC_URL% in the tags above.
2019
It will be replaced with the URL of the `public` folder during the build.
2120
Only files inside the `public` folder can be referenced from the HTML.
@@ -24,12 +23,12 @@
2423
work correctly both with client-side routing and a non-root public URL.
2524
Learn how to configure a non-root public URL by running `npm run build`.
2625
-->
27-
<title>React App</title>
28-
</head>
29-
<body>
30-
<noscript>You need to enable JavaScript to run this app.</noscript>
31-
<div id="root"></div>
32-
<!--
26+
<title>The React Quiz</title>
27+
</head>
28+
<body>
29+
<noscript>You need to enable JavaScript to run this app.</noscript>
30+
<div id="root"></div>
31+
<!--
3332
This HTML file is a template.
3433
If you open it directly in the browser, you will see an empty page.
3534
@@ -38,6 +37,5 @@
3837
3938
To begin the development, run `npm start` or `yarn start`.
4039
To create a production bundle, use `npm run build` or `yarn build`.
41-
-->
42-
</body>
40+
--></body>
4341
</html>

src/components/App.js

+95-13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { useEffect, useReducer } from 'react'
2+
import ErrorMsg from './ErrorMsg'
3+
import FinishScreen from './FinishScreen'
4+
import Footer from './Footer'
25
import Header from './Header'
3-
import Main from './Main'
46
import Loader from './Loader'
5-
import Error from './Error'
6-
import StartScreen from './StartScreen'
7+
import Main from './Main'
8+
import NextButton from './NextButton'
9+
import Progress from './Progress'
710
import Question from './Question'
11+
import StartScreen from './StartScreen'
12+
import Timer from './Timer'
13+
14+
const SEC_PER_QUESTION = 5
815

916
const initialState = {
1017
questions: [],
@@ -14,6 +21,8 @@ const initialState = {
1421
index: 0,
1522
answer: null,
1623
points: 0,
24+
highscore: 0,
25+
secondsRemaining: null,
1726
}
1827

1928
function reducer(state, action) {
@@ -33,6 +42,7 @@ function reducer(state, action) {
3342
return {
3443
...state,
3544
status: 'active',
45+
secondsRemaining: state.questions.length * SEC_PER_QUESTION,
3646
}
3747
case 'newAnswer':
3848
const question = state.questions[state.index]
@@ -47,22 +57,65 @@ function reducer(state, action) {
4757
? state.points + question.points
4858
: state.points,
4959
}
60+
case 'nextQuestion':
61+
return {
62+
...state,
63+
index: state.index + 1,
64+
answer: null,
65+
}
66+
case 'finish':
67+
return {
68+
...state,
69+
status: 'finished',
70+
highscore:
71+
state.points > state.highscore
72+
? state.points
73+
: state.highscore,
74+
}
75+
case 'restart':
76+
return {
77+
...initialState,
78+
status: 'ready',
79+
questions: state.questions,
80+
highscore: state.highscore,
81+
}
82+
case 'tick':
83+
return {
84+
...state,
85+
secondsRemaining: state.secondsRemaining - 1,
86+
status:
87+
state.secondsRemaining - 1 <= 0 ? 'finished' : state.status,
88+
}
5089
default:
51-
throw new Error(`Invalid action ${action.type}`)
90+
throw new Error(`Invalid action "${action.type}"`)
5291
}
5392
}
5493

5594
function App() {
56-
const [{ questions, status, index, answer, points }, dispatch] = useReducer(
57-
reducer,
58-
initialState,
59-
)
95+
const [
96+
{
97+
questions,
98+
status,
99+
index,
100+
answer,
101+
points,
102+
highscore,
103+
secondsRemaining,
104+
},
105+
dispatch,
106+
] = useReducer(reducer, initialState)
60107
const numQuestions = questions.length
108+
const totalPoints = questions.reduce((prev, { points }) => prev + points, 0)
61109

62110
useEffect(() => {
63111
fetch('http://localhost:8000/questions')
64112
.then((res) => res.json())
65-
.then((data) => dispatch({ type: 'dataReceived', payload: data }))
113+
.then((data) =>
114+
dispatch({
115+
type: 'dataReceived',
116+
payload: data,
117+
}),
118+
)
66119
.catch((err) => dispatch({ type: 'dataFailed' }))
67120
}, [])
68121

@@ -71,18 +124,47 @@ function App() {
71124
<Header />
72125
<Main>
73126
{status === 'loading' && <Loader />}
74-
{status === 'error' && <Error />}
127+
{status === 'error' && <ErrorMsg />}
75128
{status === 'ready' && (
76129
<StartScreen
77130
numQuestions={numQuestions}
78131
dispatch={dispatch}
79132
/>
80133
)}
81134
{status === 'active' && (
82-
<Question
83-
question={questions[index]}
135+
<>
136+
<Progress
137+
index={index}
138+
numQuestions={numQuestions}
139+
points={points}
140+
totalPoints={totalPoints}
141+
answer={answer}
142+
/>
143+
<Question
144+
question={questions[index]}
145+
dispatch={dispatch}
146+
answer={answer}
147+
/>
148+
<Footer>
149+
<Timer
150+
secondsRemaining={secondsRemaining}
151+
dispatch={dispatch}
152+
/>
153+
<NextButton
154+
dispatch={dispatch}
155+
answer={answer}
156+
index={index}
157+
numQuestions={numQuestions}
158+
/>
159+
</Footer>
160+
</>
161+
)}
162+
{status === 'finished' && (
163+
<FinishScreen
164+
points={points}
165+
totalPoints={totalPoints}
166+
highscore={highscore}
84167
dispatch={dispatch}
85-
answer={answer}
86168
/>
87169
)}
88170
</Main>

src/components/Error.js

-9
This file was deleted.

src/components/ErrorMsg.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function ErrorMsg() {
2+
return (
3+
<p className='error'>
4+
<span>💥</span> There was an error fecthing questions.
5+
</p>
6+
)
7+
}
8+
9+
export default ErrorMsg

src/components/FinishScreen.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
function FinishScreen({ points, totalPoints, highscore, dispatch }) {
2+
const percentage = (points / totalPoints) * 100
3+
4+
let emoji
5+
if (percentage === 100) emoji = '🥇'
6+
if (percentage >= 80 && percentage < 100) emoji = '🎉'
7+
if (percentage >= 50 && percentage < 80) emoji = '🙃'
8+
if (percentage >= 0 && percentage < 50) emoji = '🤨'
9+
if (percentage <= 0) emoji = '🤦‍♂️'
10+
11+
return (
12+
<>
13+
<p className='result'>
14+
{emoji} You scored <strong>{points}</strong> out of{' '}
15+
{totalPoints} ({Math.ceil(percentage)}%)
16+
</p>
17+
<p className='highscore'>(Highscore: {highscore} points)</p>
18+
<button
19+
onClick={() => dispatch({ type: 'restart' })}
20+
className='btn btn-ui'>
21+
Restart Quiz
22+
</button>
23+
</>
24+
)
25+
}
26+
export default FinishScreen

src/components/Footer.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
function Footer({ children }) {
2+
return <footer>{children}</footer>
3+
}
4+
export default Footer

src/components/NextButton.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
function NextButton({ dispatch, answer, index, numQuestions }) {
2+
if (answer === null) return null
3+
4+
if (index < numQuestions - 1)
5+
return (
6+
<button
7+
onClick={() => dispatch({ type: 'nextQuestion' })}
8+
className='btn btn-ui'>
9+
Next
10+
</button>
11+
)
12+
else
13+
return (
14+
<button
15+
onClick={() => dispatch({ type: 'finish' })}
16+
className='btn btn-ui'>
17+
Finish
18+
</button>
19+
)
20+
}
21+
export default NextButton

src/components/Progress.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function Progress({ index, numQuestions, points, totalPoints, answer }) {
2+
return (
3+
<header className='progress'>
4+
<progress
5+
max={numQuestions}
6+
value={index + Number(answer !== null)}
7+
/>
8+
<p>
9+
Question <strong>{index + 1}</strong> / {numQuestions}
10+
</p>
11+
<p>
12+
<strong>{points}</strong> / {totalPoints}
13+
</p>
14+
</header>
15+
)
16+
}
17+
export default Progress

src/components/Timer.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useEffect } from 'react'
2+
3+
function Timer({ dispatch, secondsRemaining }) {
4+
const min = Math.floor(secondsRemaining / 60)
5+
const sec = secondsRemaining % 60
6+
7+
useEffect(() => {
8+
const interval = setInterval(() => {
9+
dispatch({ type: 'tick' })
10+
}, 1000)
11+
12+
return () => clearInterval(interval)
13+
}, [dispatch])
14+
15+
return (
16+
<div className='timer'>
17+
{min < 10 && 0}
18+
{min}:{sec < 10 && 0}
19+
{sec}
20+
</div>
21+
)
22+
}
23+
export default Timer

0 commit comments

Comments
 (0)