├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── Loading.js ├── Modal.js ├── SetupForm.js ├── context.js ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/react-projects-23-quiz/a0056f79079d66a3032a080c9d635f215e882406/README.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reminder", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.21.0", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-icons": "^3.11.0", 13 | "react-scripts": "3.4.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "CI= react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/react-projects-23-quiz/a0056f79079d66a3032a080c9d635f215e882406/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Quiz Complete 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/react-projects-23-quiz/a0056f79079d66a3032a080c9d635f215e882406/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/react-projects-23-quiz/a0056f79079d66a3032a080c9d635f215e882406/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useGlobalContext } from './context' 3 | 4 | import SetupForm from './SetupForm' 5 | import Loading from './Loading' 6 | import Modal from './Modal' 7 | function App() { 8 | const { 9 | waiting, 10 | loading, 11 | questions, 12 | index, 13 | correct, 14 | nextQuestion, 15 | checkAnswer, 16 | } = useGlobalContext() 17 | if (waiting) { 18 | return 19 | } 20 | if (loading) { 21 | return 22 | } 23 | 24 | const { question, incorrect_answers, correct_answer } = questions[index] 25 | // const answers = [...incorrect_answers, correct_answer] 26 | let answers = [...incorrect_answers] 27 | const tempIndex = Math.floor(Math.random() * 4) 28 | if (tempIndex === 3) { 29 | answers.push(correct_answer) 30 | } else { 31 | answers.push(answers[tempIndex]) 32 | answers[tempIndex] = correct_answer 33 | } 34 | return ( 35 |
36 | 37 |
38 |

39 | correct answers : {correct}/{index} 40 |

41 |
42 |

43 |
44 | {answers.map((answer, index) => { 45 | return ( 46 |
55 |

56 | 59 |
60 |
61 | ) 62 | } 63 | 64 | export default App 65 | -------------------------------------------------------------------------------- /src/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |
7 |
8 | ) 9 | } 10 | 11 | export default Loading 12 | -------------------------------------------------------------------------------- /src/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useGlobalContext } from './context' 3 | 4 | const Modal = () => { 5 | const { isModalOpen, closeModal, correct, questions } = useGlobalContext() 6 | return ( 7 |
12 |
13 |

congrats!

14 |

15 | You answered {((correct / questions.length) * 100).toFixed(0)}% of 16 | questions correctly 17 |

18 | 21 |
22 |
23 | ) 24 | } 25 | 26 | export default Modal 27 | -------------------------------------------------------------------------------- /src/SetupForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useGlobalContext } from './context' 3 | 4 | const SetupForm = () => { 5 | const { quiz, handleChange, handleSubmit, error } = useGlobalContext() 6 | return ( 7 |
8 |
9 |
10 |

setup quiz

11 | {/* amount */} 12 |
13 | 14 | 24 |
25 | {/* category */} 26 | 27 |
28 | 29 | 40 |
41 | {/* difficulty */} 42 | 43 |
44 | 45 | 56 |
57 | {error && ( 58 |

59 | can't generate questions, please try different options 60 |

61 | )} 62 | 65 |
66 |
67 |
68 | ) 69 | } 70 | 71 | export default SetupForm 72 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import React, { useState, useContext, useEffect } from 'react' 3 | 4 | const table = { 5 | sports: 21, 6 | history: 23, 7 | politics: 24, 8 | } 9 | 10 | const API_ENDPOINT = 'https://opentdb.com/api.php?' 11 | 12 | const url = '' 13 | const tempUrl = 14 | 'https://opentdb.com/api.php?amount=10&category=21&difficulty=easy&type=multiple' 15 | const AppContext = React.createContext() 16 | 17 | const AppProvider = ({ children }) => { 18 | const [waiting, setWaiting] = useState(true) 19 | const [loading, setLoading] = useState(false) 20 | const [questions, setQuestions] = useState([]) 21 | const [index, setIndex] = useState(0) 22 | const [correct, setCorrect] = useState(0) 23 | const [error, setError] = useState(false) 24 | const [quiz, setQuiz] = useState({ 25 | amount: 10, 26 | category: 'sports', 27 | difficulty: 'easy', 28 | }) 29 | const [isModalOpen, setIsModalOpen] = useState(false) 30 | 31 | const fetchQuestions = async (url) => { 32 | setLoading(true) 33 | setWaiting(false) 34 | const response = await axios(url).catch((err) => console.log(err)) 35 | if (response) { 36 | const data = response.data.results 37 | if (data.length > 0) { 38 | setQuestions(data) 39 | setLoading(false) 40 | setWaiting(false) 41 | setError(false) 42 | } else { 43 | setWaiting(true) 44 | setError(true) 45 | } 46 | } else { 47 | setWaiting(true) 48 | } 49 | } 50 | 51 | const nextQuestion = () => { 52 | setIndex((oldIndex) => { 53 | const index = oldIndex + 1 54 | if (index > questions.length - 1) { 55 | openModal() 56 | return 0 57 | } else { 58 | return index 59 | } 60 | }) 61 | } 62 | const checkAnswer = (value) => { 63 | if (value) { 64 | setCorrect((oldState) => oldState + 1) 65 | } 66 | nextQuestion() 67 | } 68 | 69 | const openModal = () => { 70 | setIsModalOpen(true) 71 | } 72 | const closeModal = () => { 73 | setWaiting(true) 74 | setCorrect(0) 75 | setIsModalOpen(false) 76 | } 77 | const handleChange = (e) => { 78 | const name = e.target.name 79 | const value = e.target.value 80 | setQuiz({ ...quiz, [name]: value }) 81 | } 82 | const handleSubmit = (e) => { 83 | e.preventDefault() 84 | const { amount, category, difficulty } = quiz 85 | 86 | const url = `${API_ENDPOINT}amount=${amount}&difficulty=${difficulty}&category=${table[category]}&type=multiple` 87 | fetchQuestions(url) 88 | } 89 | 90 | return ( 91 | 108 | {children} 109 | 110 | ) 111 | } 112 | // make sure use 113 | export const useGlobalContext = () => { 114 | return useContext(AppContext) 115 | } 116 | 117 | export { AppContext, AppProvider } 118 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | /* dark shades of primary color*/ 9 | --clr-primary-1: hsl(205, 86%, 17%); 10 | --clr-primary-2: hsl(205, 77%, 27%); 11 | --clr-primary-3: hsl(205, 72%, 37%); 12 | --clr-primary-4: hsl(205, 63%, 48%); 13 | /* primary/main color */ 14 | --clr-primary-5: hsl(205, 78%, 60%); 15 | /* lighter shades of primary color */ 16 | --clr-primary-6: hsl(205, 89%, 70%); 17 | --clr-primary-7: hsl(205, 90%, 76%); 18 | --clr-primary-8: hsl(205, 86%, 81%); 19 | --clr-primary-9: hsl(205, 90%, 88%); 20 | --clr-primary-10: hsl(205, 100%, 96%); 21 | /* darkest grey - used for headings */ 22 | --clr-grey-1: hsl(209, 61%, 16%); 23 | --clr-grey-2: hsl(211, 39%, 23%); 24 | --clr-grey-3: hsl(209, 34%, 30%); 25 | --clr-grey-4: hsl(209, 28%, 39%); 26 | /* grey used for paragraphs */ 27 | --clr-grey-5: hsl(210, 22%, 49%); 28 | --clr-grey-6: hsl(209, 23%, 60%); 29 | --clr-grey-7: hsl(211, 27%, 70%); 30 | --clr-grey-8: hsl(210, 31%, 80%); 31 | --clr-grey-9: hsl(212, 33%, 89%); 32 | --clr-grey-10: hsl(210, 36%, 96%); 33 | --clr-white: #fff; 34 | --clr-red-dark: hsl(360, 67%, 44%); 35 | --clr-red-light: hsl(360, 71%, 66%); 36 | --clr-green-dark: hsl(125, 67%, 44%); 37 | --clr-green-light: hsl(125, 71%, 66%); 38 | --clr-black: #222; 39 | --transition: all 0.3s linear; 40 | --spacing: 0.1rem; 41 | --radius: 0.25rem; 42 | --light-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 43 | --dark-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); 44 | --max-width: 1170px; 45 | --fixed-width: 620px; 46 | } 47 | /* 48 | =============== 49 | Global Styles 50 | =============== 51 | */ 52 | 53 | *, 54 | ::after, 55 | ::before { 56 | margin: 0; 57 | padding: 0; 58 | box-sizing: border-box; 59 | } 60 | body { 61 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 62 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 63 | background: var(--clr-grey-10); 64 | color: var(--clr-grey-1); 65 | line-height: 1.5; 66 | font-size: 0.875rem; 67 | } 68 | ul { 69 | list-style-type: none; 70 | } 71 | a { 72 | text-decoration: none; 73 | } 74 | h1, 75 | h2, 76 | h3, 77 | h4 { 78 | letter-spacing: var(--spacing); 79 | text-transform: capitalize; 80 | line-height: 1.25; 81 | margin-bottom: 0.75rem; 82 | } 83 | h1 { 84 | font-size: 2.5rem; 85 | } 86 | h2 { 87 | font-size: 2rem; 88 | } 89 | h3 { 90 | font-size: 1.25rem; 91 | } 92 | h4 { 93 | font-size: 0.875rem; 94 | } 95 | p { 96 | margin-bottom: 1.25rem; 97 | color: var(--clr-grey-3); 98 | } 99 | @media screen and (min-width: 800px) { 100 | h1 { 101 | font-size: 3rem; 102 | } 103 | h2 { 104 | font-size: 2.5rem; 105 | } 106 | h3 { 107 | font-size: 1.75rem; 108 | } 109 | h4 { 110 | font-size: 1rem; 111 | } 112 | body { 113 | font-size: 1rem; 114 | } 115 | h1, 116 | h2, 117 | h3, 118 | h4 { 119 | line-height: 1; 120 | } 121 | } 122 | /* global classes */ 123 | 124 | /* section */ 125 | .section { 126 | width: 90vw; 127 | margin: 0 auto; 128 | max-width: var(--max-width); 129 | } 130 | 131 | @media screen and (min-width: 992px) { 132 | .section { 133 | width: 95vw; 134 | } 135 | } 136 | 137 | /* 138 | =============== 139 | Quiz 140 | =============== 141 | */ 142 | 143 | main { 144 | min-height: 100vh; 145 | display: flex; 146 | justify-content: center; 147 | align-items: center; 148 | } 149 | .quiz { 150 | width: 90vw; 151 | max-width: var(--max-width); 152 | margin: 4rem auto; 153 | background: var(--clr-white); 154 | border-radius: var(--radius); 155 | padding: 3rem; 156 | } 157 | .quiz-small { 158 | max-width: 500px; 159 | } 160 | .container h2 { 161 | margin-bottom: 2rem; 162 | text-align: center; 163 | line-height: 1.5; 164 | text-transform: none; 165 | } 166 | .answer-btn { 167 | display: block; 168 | width: 100%; 169 | margin: 0.75rem auto; 170 | background: var(--clr-primary-7); 171 | border-radius: var(--radius); 172 | border-color: transparent; 173 | color: var(--clr-black); 174 | letter-spacing: var(--spacing); 175 | font-size: 1rem; 176 | cursor: pointer; 177 | padding: 0.5rem 0; 178 | transition: var(--transition); 179 | } 180 | @media screen and (min-width: 576px) { 181 | .answer-btn { 182 | max-width: 60%; 183 | } 184 | } 185 | .answer-btn:hover { 186 | background: var(--clr-primary-5); 187 | color: var(--clr-white); 188 | } 189 | .correct-answers { 190 | font-size: 1rem; 191 | margin-bottom: 2rem; 192 | text-align: right; 193 | text-transform: capitalize; 194 | letter-spacing: var(--spacing); 195 | color: var(--clr-green-dark); 196 | } 197 | .next-question, 198 | .close-btn, 199 | .submit-btn { 200 | border-radius: var(--radius); 201 | border-color: transparent; 202 | padding: 0.25rem 0.5rem; 203 | display: block; 204 | width: 35%; 205 | margin-left: auto; 206 | margin-top: 2rem; 207 | text-transform: capitalize; 208 | font-weight: 700; 209 | letter-spacing: var(--spacing); 210 | font-size: 1rem; 211 | background: #ffb100; 212 | color: var(--clr-black); 213 | transition: var(--transition); 214 | cursor: pointer; 215 | } 216 | .next-question:hover { 217 | background: #805900; 218 | color: #ffb100; 219 | } 220 | /* 221 | =============== 222 | Form 223 | =============== 224 | */ 225 | 226 | .setup-form h2 { 227 | margin-bottom: 2rem; 228 | } 229 | .form-control { 230 | margin-bottom: 2rem; 231 | } 232 | .form-control label { 233 | display: block; 234 | text-transform: capitalize; 235 | font-weight: 500; 236 | color: var(--clr-grey-3); 237 | margin-bottom: 0.5rem; 238 | } 239 | .form-input { 240 | border: none; 241 | background: var(--clr-grey-10); 242 | font-size: 1rem; 243 | padding: 0.25rem 0.5rem; 244 | width: 100%; 245 | border-radius: var(--radius); 246 | } 247 | .error { 248 | color: var(--clr-red-dark); 249 | text-transform: capitalize; 250 | } 251 | .submit-btn { 252 | width: 100%; 253 | margin-top: 3rem; 254 | } 255 | /* 256 | =============== 257 | Loading 258 | =============== 259 | */ 260 | @keyframes spinner { 261 | to { 262 | transform: rotate(360deg); 263 | } 264 | } 265 | 266 | .loading { 267 | width: 6rem; 268 | height: 6rem; 269 | margin: 0 auto; 270 | margin-top: 10rem; 271 | border-radius: 50%; 272 | border: 3px solid #ccc; 273 | border-top-color: var(--clr-primary-5); 274 | animation: spinner 0.6s linear infinite; 275 | } 276 | 277 | .modal-container { 278 | position: fixed; 279 | top: 0; 280 | left: 0; 281 | width: 100%; 282 | height: 100%; 283 | background: rgba(0, 0, 0, 0.75); 284 | display: flex; 285 | align-items: center; 286 | justify-content: center; 287 | opacity: 0; 288 | transition: var(--transition); 289 | z-index: -1; 290 | } 291 | .isOpen { 292 | opacity: 1; 293 | z-index: 999; 294 | } 295 | 296 | .modal-content { 297 | background: var(--clr-white); 298 | width: 90vw; 299 | max-width: var(--fixed-width); 300 | padding: 3rem; 301 | border-radius: var(--radius); 302 | text-align: center; 303 | position: relative; 304 | } 305 | .modal-content p { 306 | font-size: 1.5rem; 307 | text-transform: none; 308 | } 309 | .close-btn { 310 | margin-right: auto; 311 | } 312 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import { AppProvider } from './context' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ) 15 | --------------------------------------------------------------------------------