ready to play

This commit is contained in:
2023-06-03 00:22:29 +02:00
parent fa20932e18
commit 4114ccb678
22 changed files with 29784 additions and 1 deletions
+23
View File
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+6
View File
@@ -0,0 +1,6 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80
}
+2
View File
@@ -0,0 +1,2 @@
{
}
+35 -1
View File
@@ -1 +1,35 @@
# react-quiz-app # React Shop
![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![Styled Components](https://img.shields.io/badge/styled--components-DB7093?style=for-the-badge&logo=styled-components&logoColor=white)
---
## Description
---
The React Quiz App does exactly what you would expect. Depending on the level, number, and category, you receive a list of questions from [Open Trivia Database](https://opentdb.com) to test yourself. The app is created with React and Typescript and the styles are made with styled-components.
## Installation
---
### Under the repository name, click on clone or download. In the terminal paste this command:
```git clone https://dadobos.github.io/react-quiz-app.git```
### Open the project in VSCode or move inside the folder with:
```cd react-quiz-app```
### Install the npm packages:
```npm install```
### Run the application with:
```npm start```
### In the browser open the "3000" port:
```http://localhost:3000/```
+29071
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
{
"name": "react-quiz-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.11",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/styled-components": "^5.1.26",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"styled-components": "^5.3.6",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
+54
View File
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#335c67" />
<meta name="description" content="Quiz app" />
<link
rel="icon"
href="https://res.cloudinary.com/dadobos/image/upload/v1642766520/web/favicon_ugvc88.ico"
type="image/x-icon"
/>
<link
rel="preload"
as="style"
crossorigin="anonymous"
href="https://fonts.googleapis.com/css2?family=Nanum+Gothic+Coding:wght@400;700&display=swap"
/>
<link
rel="stylesheet"
crossorigin="anonymous"
href="https://fonts.googleapis.com/css2?family=Nanum+Gothic+Coding:wght@400;700&display=swap"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React Quiz App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
{
"short_name": "Quiz App",
"name": "React Quiz App",
"icons": [],
"start_url": ".",
"display": "standalone",
"theme_color": "#335c67",
"background_color": "#335c67"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+34
View File
@@ -0,0 +1,34 @@
import { shuffleArray } from './Utils';
import { QuestionType } from './Types';
export const fetchQuizQuestions = async (
amount: number,
category: number,
difficulty: string
) => {
try {
let anyCategory: string = category === 1 ? '' : '&category=' + category;
const endpoint = `https://opentdb.com/api.php?amount=${amount}${anyCategory}&difficulty=${difficulty}&type=multiple`;
const data = await (await fetch(endpoint)).json();
return data.results.map((question: QuestionType) => ({
...question,
answers: shuffleArray([
...question.incorrect_answers,
question.correct_answer
])
}));
} catch (error) {
console.log(error);
}
};
export const fetchCategories = async () => {
try {
const endpoint = `https://opentdb.com/api_category.php`;
const response = await fetch(endpoint);
const categories = await response.json();
return categories.trivia_categories;
} catch (error) {
console.log(error);
}
};
+122
View File
@@ -0,0 +1,122 @@
import styled, { createGlobalStyle } from 'styled-components';
export const GlobalStyle = createGlobalStyle`
// reset
* {
font-family: "Nanum Gothic Coding", monospace;
box-sizing: border-box;
margin:0;
padding:0;
}
html {
min-height:100vh;
}
body {
margin:0;
padding:0;
height: 100%;
min-height:100vh;
background-color:#22363c;
color:#009688;
display: flex;
flex-direction: column;
align-items: center;
}`;
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
padding: 0.5rem;
color: white;
width: 70rem;
max-width: 100vw;
h1 {
font-size: 3rem;
text-align: center;
margin-bottom: 2rem;
@media (max-width: 768px) {
font-size: 2rem;
}
}
.question-card-wrapper {
width: 100%;
}
.details {
margin: 2rem 0;
font-size: 1.4rem;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
@media (max-width: 768px) {
flex-direction: column;
}
}
.question {
margin: 2rem 0;
font-size: 1.2rem;
}
.start,
.next {
cursor: pointer;
padding: 1rem;
margin-top: 2rem;
margin-bottom: 2rem;
background-color: #009688;
font-size: 1.4rem;
color: white;
border: 1px transparent;
width: 100%;
}
`;
type ButtonWrapperProps = {
correct: boolean;
userClicked: boolean;
};
export const ButtonWrapper = styled.div<ButtonWrapperProps>`
transition: all 0.3s ease;
:hover {
opacity: 0.8;
}
button {
cursor: pointer;
user-select: none;
font-size: 1.4rem;
color: white;
width: 100%;
max-width: 100vw;
margin: 5px 0;
background: ${({ correct, userClicked }) =>
correct ? '#84c318' : !correct && userClicked ? '#92374d' : '#22363c'};
@media (max-width: 768px) {
font-size: 1.2rem;
margin: 8px 0;
}
}
`;
export const SelectWrapper = styled.select`
width: 100%;
max-width: 70rem;
font-size: 1.6rem;
background-color: #22363c;
color: white;
padding: 0.24rem;
margin-top: 1rem;
text-align: center;
:hover {
opacity: 0.8;
}
`;
+133
View File
@@ -0,0 +1,133 @@
import React, { useState, useEffect } from 'react';
import { fetchQuizQuestions, fetchCategories } from './API';
import QuestionCard from './components/QuestionCard';
import { GlobalStyle, Wrapper } from './App.style';
import SelectDifficulty, { Difficulty } from './components/SelectDifficulty';
import SelectQuestions, { TotalQuestions } from './components/SelectQuestions';
import SelectCategory from './components/SelectCategory';
import { Category, QuestionState, AnswerObject } from './Types';
const App = () => {
const [loading, setLoading] = useState(false);
const [questions, setQuestions] = useState<QuestionState[]>([]);
const [number, setNumber] = useState(0);
const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([]);
const [score, setScore] = useState(0);
const [gameOver, setGameOver] = useState(true);
const [difficulty, setDifficulty] = useState<string>(Difficulty.EASY);
const [totalQuestionNumber, setTotalQuestionNumber] = useState<number>(
TotalQuestions.TEN
);
const [category, setCategory] = useState<number>(1);
const [categoryList, setCategoryList] = useState<Array<Category>>([]);
useEffect(() => {
fetchCategories().then((data) => setCategoryList(data));
}, []);
const startTrivia = async () => {
setLoading(true);
setGameOver(false);
const newQuestions = await fetchQuizQuestions(
totalQuestionNumber,
category,
difficulty
);
setQuestions(newQuestions);
setScore(0);
setUserAnswers([]);
setNumber(0);
setLoading(false);
};
const checkAnswer = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!gameOver) {
const answer = e.currentTarget.value;
const correct = questions[number].correct_answer === answer;
if (correct) {
setScore((prev) => prev + 1);
}
const answerObject = {
question: questions[number].question,
answer,
correct,
correctAnswer: questions[number].correct_answer
};
setUserAnswers((prev) => [...prev, answerObject]);
}
};
const nextQuestion = () => {
const nextNumber = number + 1;
if (nextNumber === totalQuestionNumber) {
setGameOver(true);
} else {
setNumber(nextNumber);
}
};
return (
<>
<GlobalStyle />
<Wrapper className="App">
<h1>Quiz</h1>
{gameOver || number + 1 === totalQuestionNumber ? (
<div>
<SelectDifficulty
difficulty={difficulty}
setDifficulty={setDifficulty}
/>
<SelectQuestions
totalQuestionNumber={totalQuestionNumber}
setTotalQuestionNumber={setTotalQuestionNumber}
/>
<SelectCategory
category={category}
categoryList={categoryList}
setCategory={setCategory}
/>
<button className="start" onClick={startTrivia}>
Start
{number + 1 === totalQuestionNumber && ' again'}
</button>
</div>
) : null}
{loading ?? <p>Load Questions...</p>}
{!loading && !gameOver && (
<QuestionCard
difficulty={difficulty}
score={score}
questionNr={number + 1}
totalQuestions={totalQuestionNumber}
question={questions[number].question}
answers={questions[number].answers}
category={questions[number].category}
userAnswer={userAnswers ? userAnswers[number] : undefined}
callback={checkAnswer}
/>
)}
{!gameOver &&
!loading &&
userAnswers.length === number + 1 &&
number + 1 !== totalQuestionNumber ? (
<button className="next" onClick={nextQuestion}>
Next Question
</button>
) : null}
</Wrapper>
</>
);
};
export default App;
+21
View File
@@ -0,0 +1,21 @@
export type Category = {
id: number;
name: string;
};
export type QuestionType = {
category: string;
correct_answer: string;
incorrect_answers: string[];
question: string;
type: string;
};
export type QuestionState = QuestionType & { answers: string[] };
export type AnswerObject = {
question: string;
answer: string;
correct: boolean;
correctAnswer: string;
};
+11
View File
@@ -0,0 +1,11 @@
export const shuffleArray = (array: any[]) =>
[...array].sort(() => Math.random() - 0.5);
export const Capitalize = (text: string) => {
return text
.replace(/[_-]/g, ' ')
.replace(
/(^\w|\s\w)(\S*)/g,
(_, m1, m2) => m1.toUpperCase() + m2.toLowerCase()
);
};
+65
View File
@@ -0,0 +1,65 @@
import React from 'react';
import { ButtonWrapper } from '../App.style';
import { AnswerObject } from '../Types';
import { Capitalize } from '../Utils';
type Props = {
answers: string[];
callback: (e: React.MouseEvent<HTMLButtonElement>) => void;
difficulty: string;
question: string;
category: string;
questionNr: number;
score: number;
totalQuestions: number;
userAnswer: AnswerObject | undefined;
};
const QuestionCard: React.FC<Props> = ({
answers,
callback,
difficulty,
question,
category,
questionNr,
score,
totalQuestions,
userAnswer
}) => (
<div className="question-card-wrapper">
<div className="details">
<div>Difficulty: {Capitalize(difficulty)}</div>
<div>{category}</div>
</div>
<div className="details">
<div>
Question: {questionNr}/{totalQuestions}
</div>
<div>Score: {score}</div>
</div>
{!(questionNr === totalQuestions) && (
<>
<p
className="question"
dangerouslySetInnerHTML={{ __html: question }}
/>
<div>
{answers.map((answer) => (
<ButtonWrapper
correct={userAnswer?.correctAnswer === answer}
userClicked={userAnswer?.answer === answer}
key={answer}
>
<button disabled={!!userAnswer} onClick={callback} value={answer}>
<span dangerouslySetInnerHTML={{ __html: answer }} />
</button>
</ButtonWrapper>
))}
</div>
</>
)}
</div>
);
export default QuestionCard;
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import { SelectWrapper } from '../App.style';
import { Category } from '../Types';
type Props = {
category: number;
categoryList: Array<Category>;
setCategory: (category: number) => void;
};
const SelectCategory = ({ category, categoryList, setCategory }: Props) => {
return (
<SelectWrapper
value={category}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
e.preventDefault();
setCategory(e.target.value as number);
}}
>
<option value={1}>Any Category</option>
{categoryList.map((cat) => (
<option value={cat.id} key={cat.id}>
{cat.name}
</option>
))}
</SelectWrapper>
);
};
export default SelectCategory;
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import { SelectWrapper } from '../App.style';
export enum Difficulty {
EASY = 'easy',
MEDIUM = 'medium',
HARD = 'hard'
}
type Props = {
difficulty: string;
setDifficulty: (difficulty: string) => void;
};
const SelectDifficulty = ({ difficulty, setDifficulty }: Props) => {
return (
<SelectWrapper
value={difficulty}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
e.preventDefault();
setDifficulty(e.target.value);
}}
>
<option value={Difficulty.EASY}>Easy</option>
<option value={Difficulty.MEDIUM}>Medium</option>
<option value={Difficulty.HARD}>Hard</option>
</SelectWrapper>
);
};
export default SelectDifficulty;
+42
View File
@@ -0,0 +1,42 @@
import React from 'react';
import { SelectWrapper } from '../App.style';
export enum TotalQuestions {
FIVE = 5,
TEN = 10,
FIFTEEN = 15,
TWENTY = 20,
TWENTYFIVE = 25,
FIFTY = 50
}
type Props = {
totalQuestionNumber: number;
setTotalQuestionNumber: (totalQuestionNumber: number) => void;
};
const SelectQuestions = ({
totalQuestionNumber,
setTotalQuestionNumber
}: Props) => {
return (
<SelectWrapper
value={totalQuestionNumber}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
e.preventDefault();
setTotalQuestionNumber(e.target.value as number);
}}
>
<option value={TotalQuestions.FIVE}>{TotalQuestions.FIVE}</option>
<option value={TotalQuestions.TEN}>{TotalQuestions.TEN}</option>
<option value={TotalQuestions.FIFTEEN}>{TotalQuestions.FIFTEEN}</option>
<option value={TotalQuestions.TWENTY}>{TotalQuestions.TWENTY}</option>
<option value={TotalQuestions.TWENTYFIVE}>
{TotalQuestions.TWENTYFIVE}
</option>
<option value={TotalQuestions.FIFTY}>{TotalQuestions.FIFTY}</option>
</SelectWrapper>
);
};
export default SelectQuestions;
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+1
View File
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
+5
View File
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2016",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}