ready to play
This commit is contained in:
+23
@@ -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*
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
@@ -1 +1,35 @@
|
|||||||
# react-quiz-app
|
# React Shop
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
## 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/```
|
||||||
|
|||||||
Generated
+29071
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Quiz App",
|
||||||
|
"name": "React Quiz App",
|
||||||
|
"icons": [],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#335c67",
|
||||||
|
"background_color": "#335c67"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
+34
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
@@ -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;
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
||||||
@@ -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';
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user