├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── API.ts ├── App.styles.ts ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── QuestionCard.styles.ts │ └── QuestionCard.tsx ├── images │ ├── nattu-adnan-unsplash.jpg │ ├── quiz-logo.png │ └── quiz-logo.svg ├── main.tsx ├── utils.ts └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-quiz", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tanstack/react-query": "^5.50.1", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1", 16 | "styled-components": "^6.1.11" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.3.3", 20 | "@types/react-dom": "^18.3.0", 21 | "@typescript-eslint/eslint-plugin": "^7.13.1", 22 | "@typescript-eslint/parser": "^7.13.1", 23 | "@vitejs/plugin-react": "^4.3.1", 24 | "eslint": "^8.57.0", 25 | "eslint-plugin-react-hooks": "^4.6.2", 26 | "eslint-plugin-react-refresh": "^0.4.7", 27 | "typescript": "^5.2.2", 28 | "vite": "^5.3.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/API.ts: -------------------------------------------------------------------------------- 1 | import { shuffleArray } from './utils'; 2 | 3 | export type Question = { 4 | category: string; 5 | correct_answer: string; 6 | difficulty: string; 7 | incorrect_answers: string[]; 8 | question: string; 9 | type: string; 10 | }; 11 | 12 | export enum Difficulty { 13 | EASY = "easy", 14 | MEDIUM = "medium", 15 | HARD = "hard", 16 | } 17 | 18 | export type QuestionsState = Question & { answers: string[] }; 19 | 20 | export const fetchQuizQuestions = async (amount: number, difficulty: Difficulty): Promise => { 21 | const endpoint = `https://opentdb.com/api.php?amount=${amount}&difficulty=${difficulty}&type=multiple`; 22 | const data = await (await fetch(endpoint)).json(); 23 | return data.results.map((question: Question) => ({ 24 | ...question, 25 | answers: shuffleArray([...question.incorrect_answers, question.correct_answer]) 26 | })) 27 | }; 28 | -------------------------------------------------------------------------------- /src/App.styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { createGlobalStyle } from 'styled-components'; 2 | import BGImage from './images/nattu-adnan-unsplash.jpg'; 3 | 4 | export const GlobalStyle = createGlobalStyle` 5 | html { 6 | height: 100%; 7 | } 8 | 9 | body { 10 | background-image: url(${BGImage}); 11 | background-size: cover; 12 | margin: 0; 13 | padding: 0 20px; 14 | display: flex; 15 | justify-content: center; 16 | } 17 | 18 | * { 19 | font-family: 'Catamaran', sans-serif; 20 | box-sizing: border-box; 21 | } 22 | `; 23 | 24 | export const Wrapper = styled.div` 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | 29 | > p { 30 | color: #fff; 31 | } 32 | 33 | .score { 34 | color: #fff; 35 | font-size: 2rem; 36 | margin: 0; 37 | } 38 | 39 | h1 { 40 | font-family: Fascinate Inline; 41 | background-image: linear-gradient(180deg, #fff, #87f1ff); 42 | font-weight: 400; 43 | background-size: 100%; 44 | background-clip: text; 45 | -webkit-background-clip: text; 46 | -webkit-text-fill-color: transparent; 47 | -moz-background-clip: text; 48 | -moz-text-fill-color: transparent; 49 | filter: drop-shadow(2px 2px #0085a3); 50 | font-size: 70px; 51 | text-align: center; 52 | margin: 20px; 53 | } 54 | 55 | .start, .next { 56 | cursor: pointer; 57 | background: linear-gradient(180deg, #ffffff, #ffcc91); 58 | border: 2px solid #d38558; 59 | box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.25); 60 | border-radius: 10px; 61 | height: 40px; 62 | margin: 20px 0; 63 | padding: 0 40px; 64 | } 65 | 66 | .start { 67 | max-width: 200px; 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { fetchQuizQuestions } from './API'; 3 | // Components 4 | import QuestionCard from './components/QuestionCard'; 5 | // types 6 | import { QuestionsState, Difficulty } from './API'; 7 | // Styles 8 | import { GlobalStyle, Wrapper } from './App.styles'; 9 | 10 | export type AnswerObject = { 11 | question: string; 12 | answer: string; 13 | correct: boolean; 14 | correctAnswer: string; 15 | }; 16 | 17 | const TOTAL_QUESTIONS = 10; 18 | 19 | const App: React.FC = () => { 20 | const [loading, setLoading] = useState(false); 21 | const [questions, setQuestions] = useState([]); 22 | const [number, setNumber] = useState(0); 23 | const [userAnswers, setUserAnswers] = useState([]); 24 | const [score, setScore] = useState(0); 25 | const [gameOver, setGameOver] = useState(true); 26 | 27 | const startTrivia = async () => { 28 | setLoading(true); 29 | setGameOver(false); 30 | const newQuestions = await fetchQuizQuestions( 31 | TOTAL_QUESTIONS, 32 | Difficulty.EASY 33 | ); 34 | setQuestions(newQuestions); 35 | setScore(0); 36 | setUserAnswers([]); 37 | setNumber(0); 38 | setLoading(false); 39 | }; 40 | 41 | const checkAnswer = (e: React.MouseEvent) => { 42 | if (!gameOver) { 43 | // User's answer 44 | const answer = e.currentTarget.value; 45 | // Check answer against correct answer 46 | const correct = questions[number].correct_answer === answer; 47 | // Add score if answer is correct 48 | if (correct) setScore((prev) => prev + 1); 49 | // Save the answer in the array for user answers 50 | const answerObject = { 51 | question: questions[number].question, 52 | answer, 53 | correct, 54 | correctAnswer: questions[number].correct_answer, 55 | }; 56 | setUserAnswers((prev) => [...prev, answerObject]); 57 | } 58 | }; 59 | 60 | const nextQuestion = () => { 61 | // Move on to the next question if not the last question 62 | const nextQ = number + 1; 63 | 64 | if (nextQ === TOTAL_QUESTIONS) { 65 | setGameOver(true); 66 | } else { 67 | setNumber(nextQ); 68 | } 69 | }; 70 | 71 | return ( 72 | <> 73 | 74 | 75 |

REACT QUIZ

76 | {gameOver || userAnswers.length === TOTAL_QUESTIONS ? ( 77 | 80 | ) : null} 81 | {!gameOver ?

Score: {score}

: null} 82 | {loading ?

Loading Questions...

: null} 83 | {!loading && !gameOver && ( 84 | 92 | )} 93 | {!gameOver && !loading && userAnswers.length === number + 1 && number !== TOTAL_QUESTIONS - 1 ? ( 94 | 97 | ) : null} 98 |
99 | 100 | ); 101 | }; 102 | 103 | export default App; 104 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/QuestionCard.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | max-width: 1100px; 5 | background: #ebfeff; 6 | border-radius: 10px; 7 | border: 2px solid #0085a3; 8 | padding: 20px; 9 | box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.25); 10 | text-align: center; 11 | 12 | p { 13 | font-size: 1rem; 14 | } 15 | `; 16 | 17 | type ButtonWrapperProps = { 18 | $correct: boolean; 19 | $userClicked: boolean; 20 | }; 21 | 22 | export const ButtonWrapper = styled.div` 23 | transition: all 0.3s ease; 24 | 25 | :hover { 26 | opacity: 0.8; 27 | } 28 | 29 | button { 30 | cursor: pointer; 31 | user-select: none; 32 | font-size: 0.8rem; 33 | width: 100%; 34 | height: 40px; 35 | margin: 5px 0; 36 | background: ${({ $correct, $userClicked }) => 37 | $correct 38 | ? 'linear-gradient(90deg, #56FFA4, #59BC86)' 39 | : !$correct && $userClicked 40 | ? 'linear-gradient(90deg, #FF5656, #C16868)' 41 | : 'linear-gradient(90deg, #56ccff, #6eafb4)'}; 42 | border: 3px solid #ffffff; 43 | box-shadow: 1px 2px 0px rgba(0, 0, 0, 0.1); 44 | border-radius: 10px; 45 | color: #fff; 46 | text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.25); 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /src/components/QuestionCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // Types 3 | import { AnswerObject } from '../App'; 4 | // Styles 5 | import { Wrapper, ButtonWrapper } from './QuestionCard.styles'; 6 | 7 | type Props = { 8 | question: string; 9 | answers: string[]; 10 | callback: (e: React.MouseEvent) => void; 11 | userAnswer: AnswerObject | undefined; 12 | questionNr: number; 13 | totalQuestions: number; 14 | }; 15 | 16 | const QuestionCard: React.FC = ({ 17 | question, 18 | answers, 19 | callback, 20 | userAnswer, 21 | questionNr, 22 | totalQuestions, 23 | }) => ( 24 | 25 |

26 | Question: {questionNr} / {totalQuestions} 27 |

28 |

29 |

30 | {answers.map((answer) => ( 31 | 36 | 39 | 40 | ))} 41 |
42 |
43 | ); 44 | 45 | export default QuestionCard; 46 | -------------------------------------------------------------------------------- /src/images/nattu-adnan-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weibenfalk/react-quiz/088fde6f48daac9e8aed2e07d17cb22b8d789b72/src/images/nattu-adnan-unsplash.jpg -------------------------------------------------------------------------------- /src/images/quiz-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weibenfalk/react-quiz/088fde6f48daac9e8aed2e07d17cb22b8d789b72/src/images/quiz-logo.png -------------------------------------------------------------------------------- /src/images/quiz-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const shuffleArray = (array: any[]) => 2 | [...array].sort(() => Math.random() - 0.5); 3 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------