├── .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 |
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 |
--------------------------------------------------------------------------------