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
+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';