added error boundary, fixed selection and layout
This commit is contained in:
+5
-3
@@ -41,7 +41,8 @@ export const Wrapper = styled.div`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
font-size: 2rem;
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,13 +51,14 @@ export const Wrapper = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
margin: 2rem 0;
|
margin: .3rem 0;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ export const ButtonWrapper = styled.div<ButtonWrapperProps>`
|
|||||||
export const SelectWrapper = styled.select`
|
export const SelectWrapper = styled.select`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 70rem;
|
max-width: 70rem;
|
||||||
font-size: 1.6rem;
|
font-size: 1.4rem;
|
||||||
background-color: #22363c;
|
background-color: #22363c;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.24rem;
|
padding: 0.24rem;
|
||||||
|
|||||||
+35
-29
@@ -5,25 +5,26 @@ import { GlobalStyle, Wrapper } from './App.style';
|
|||||||
import SelectDifficulty, { Difficulty } from './components/SelectDifficulty';
|
import SelectDifficulty, { Difficulty } from './components/SelectDifficulty';
|
||||||
import SelectQuestions, { TotalQuestions } from './components/SelectQuestions';
|
import SelectQuestions, { TotalQuestions } from './components/SelectQuestions';
|
||||||
import SelectCategory from './components/SelectCategory';
|
import SelectCategory from './components/SelectCategory';
|
||||||
|
import DetailsCard from './components/DetailsCard';
|
||||||
import { Category, QuestionState, AnswerObject } from './Types';
|
import { Category, QuestionState, AnswerObject } from './Types';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [questions, setQuestions] = useState<QuestionState[]>([]);
|
const [questions, setQuestions] = useState<QuestionState[]>([]);
|
||||||
const [number, setNumber] = useState(0);
|
const [questionNumber, setQuestionNumber] = useState(0);
|
||||||
const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([]);
|
const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([]);
|
||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
const [gameOver, setGameOver] = useState(true);
|
const [gameOver, setGameOver] = useState<boolean>(true);
|
||||||
const [difficulty, setDifficulty] = useState<string>(Difficulty.EASY);
|
const [difficulty, setDifficulty] = useState<string>(Difficulty.EASY);
|
||||||
const [totalQuestionNumber, setTotalQuestionNumber] = useState<number>(
|
const [totalQuestionNumber, setTotalQuestionNumber] = useState<number>(
|
||||||
TotalQuestions.TEN
|
TotalQuestions.TEN
|
||||||
);
|
);
|
||||||
|
|
||||||
const [category, setCategory] = useState<number>(1);
|
const [categoryID, setCategoryID] = useState<number>(1);
|
||||||
const [categoryList, setCategoryList] = useState<Array<Category>>([]);
|
const [categoryList, setCategoryList] = useState<Array<Category>>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories().then((data) => setCategoryList(data));
|
fetchCategories().then((categories) => setCategoryList(categories));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startTrivia = async () => {
|
const startTrivia = async () => {
|
||||||
@@ -32,14 +33,14 @@ const App = () => {
|
|||||||
|
|
||||||
const newQuestions = await fetchQuizQuestions(
|
const newQuestions = await fetchQuizQuestions(
|
||||||
totalQuestionNumber,
|
totalQuestionNumber,
|
||||||
category,
|
categoryID,
|
||||||
difficulty
|
difficulty
|
||||||
);
|
);
|
||||||
|
|
||||||
setQuestions(newQuestions);
|
|
||||||
setScore(0);
|
setScore(0);
|
||||||
setUserAnswers([]);
|
setUserAnswers([]);
|
||||||
setNumber(0);
|
setQuestionNumber(0);
|
||||||
|
setQuestions(newQuestions);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,17 +48,16 @@ const App = () => {
|
|||||||
if (!gameOver) {
|
if (!gameOver) {
|
||||||
const answer = e.currentTarget.value;
|
const answer = e.currentTarget.value;
|
||||||
|
|
||||||
const correct = questions[number].correct_answer === answer;
|
const correct = questions[questionNumber].correct_answer === answer;
|
||||||
|
|
||||||
if (correct) {
|
if (correct) {
|
||||||
setScore((prev) => prev + 1);
|
setScore((prev) => prev + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const answerObject = {
|
const answerObject = {
|
||||||
question: questions[number].question,
|
question: questions[questionNumber].question,
|
||||||
answer,
|
answer,
|
||||||
correct,
|
correct,
|
||||||
correctAnswer: questions[number].correct_answer
|
correctAnswer: questions[questionNumber].correct_answer
|
||||||
};
|
};
|
||||||
|
|
||||||
setUserAnswers((prev) => [...prev, answerObject]);
|
setUserAnswers((prev) => [...prev, answerObject]);
|
||||||
@@ -65,11 +65,10 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const nextQuestion = () => {
|
const nextQuestion = () => {
|
||||||
const nextNumber = number + 1;
|
if (questionNumber + 1 >= totalQuestionNumber) {
|
||||||
if (nextNumber === totalQuestionNumber) {
|
|
||||||
setGameOver(true);
|
setGameOver(true);
|
||||||
} else {
|
} else {
|
||||||
setNumber(nextNumber);
|
setQuestionNumber(questionNumber + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ const App = () => {
|
|||||||
<Wrapper className="App">
|
<Wrapper className="App">
|
||||||
<h1>Quiz</h1>
|
<h1>Quiz</h1>
|
||||||
|
|
||||||
{gameOver || number + 1 === totalQuestionNumber ? (
|
{gameOver || questionNumber === totalQuestionNumber ? (
|
||||||
<div>
|
<div>
|
||||||
<SelectDifficulty
|
<SelectDifficulty
|
||||||
difficulty={difficulty}
|
difficulty={difficulty}
|
||||||
@@ -90,37 +89,44 @@ const App = () => {
|
|||||||
setTotalQuestionNumber={setTotalQuestionNumber}
|
setTotalQuestionNumber={setTotalQuestionNumber}
|
||||||
/>
|
/>
|
||||||
<SelectCategory
|
<SelectCategory
|
||||||
category={category}
|
categoryID={categoryID}
|
||||||
categoryList={categoryList}
|
categoryList={categoryList}
|
||||||
setCategory={setCategory}
|
setCategoryID={setCategoryID}
|
||||||
/>
|
/>
|
||||||
<button className="start" onClick={startTrivia}>
|
<button className="start" onClick={startTrivia}>
|
||||||
Start
|
Start
|
||||||
{number + 1 === totalQuestionNumber && ' again'}
|
{questionNumber + 1 === totalQuestionNumber && ' again'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{loading ?? <p>Load Questions...</p>}
|
{loading ?? <p>Loading Questions...</p>}
|
||||||
|
{
|
||||||
|
<DetailsCard
|
||||||
|
gameOver={gameOver}
|
||||||
|
difficulty={difficulty}
|
||||||
|
category={
|
||||||
|
questions ? questions[questionNumber]?.category : undefined
|
||||||
|
}
|
||||||
|
score={score}
|
||||||
|
questionNumber={questionNumber}
|
||||||
|
totalQuestions={totalQuestionNumber}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
{!loading && !gameOver && (
|
{!loading && !gameOver && (
|
||||||
<QuestionCard
|
<QuestionCard
|
||||||
difficulty={difficulty}
|
question={questions[questionNumber].question}
|
||||||
score={score}
|
answers={questions[questionNumber].answers}
|
||||||
questionNr={number + 1}
|
userAnswers={userAnswers ? userAnswers[questionNumber] : undefined}
|
||||||
totalQuestions={totalQuestionNumber}
|
|
||||||
question={questions[number].question}
|
|
||||||
answers={questions[number].answers}
|
|
||||||
category={questions[number].category}
|
|
||||||
userAnswer={userAnswers ? userAnswers[number] : undefined}
|
|
||||||
callback={checkAnswer}
|
callback={checkAnswer}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!gameOver &&
|
{!gameOver &&
|
||||||
!loading &&
|
!loading &&
|
||||||
userAnswers.length === number + 1 &&
|
userAnswers.length === questionNumber + 1 &&
|
||||||
number + 1 !== totalQuestionNumber ? (
|
questionNumber !== totalQuestionNumber ? (
|
||||||
<button className="next" onClick={nextQuestion}>
|
<button className="next" onClick={nextQuestion}>
|
||||||
Next Question
|
Next Question
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,3 +9,18 @@ export const Capitalize = (text: string) => {
|
|||||||
(_, m1, m2) => m1.toUpperCase() + m2.toLowerCase()
|
(_, m1, m2) => m1.toUpperCase() + m2.toLowerCase()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const unEscape = (text: string) => {
|
||||||
|
text = text.replace(/'/g, "'");
|
||||||
|
text = text.replace(/á/g, 'á');
|
||||||
|
text = text.replace(/&/g, '&');
|
||||||
|
text = text.replace(/å/g, 'å');
|
||||||
|
text = text.replace(/ä/g, 'ä');
|
||||||
|
text = text.replace(/>/g, '>');
|
||||||
|
text = text.replace(/</g, '<');
|
||||||
|
text = text.replace(/ñ/g, 'ñ');
|
||||||
|
text = text.replace(/ö/g, 'ö');
|
||||||
|
text = text.replace(/"/g, '"');
|
||||||
|
text = text.replace(/ü/g, 'ü');
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Capitalize } from '../Utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
difficulty: string;
|
||||||
|
category: string | undefined;
|
||||||
|
questionNumber: number;
|
||||||
|
totalQuestions: number;
|
||||||
|
score: number;
|
||||||
|
gameOver:boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailsCard: React.FC<Props> = ({
|
||||||
|
difficulty,
|
||||||
|
category,
|
||||||
|
questionNumber,
|
||||||
|
totalQuestions,
|
||||||
|
score,
|
||||||
|
gameOver
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="question-card-wrapper">
|
||||||
|
<div className="details">
|
||||||
|
<div>Difficulty: {Capitalize(difficulty)}</div>
|
||||||
|
<div>
|
||||||
|
Question: {!gameOver && questionNumber + 1+'\/'}{totalQuestions}
|
||||||
|
</div>
|
||||||
|
<div>Score: {score}</div>
|
||||||
|
{category && (
|
||||||
|
<div>
|
||||||
|
Category: {category.replace(/Entertainment: |Science: /g, '')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailsCard;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { Component, ErrorInfo, ReactNode } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
hasError: false
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(_: Error): State {
|
||||||
|
// Update state so the next render will show the fallback UI.
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error("Uncaught error:", error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return <h1>Sorry.. there was an error</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -1,64 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ButtonWrapper } from '../App.style';
|
import { ButtonWrapper } from '../App.style';
|
||||||
import { AnswerObject } from '../Types';
|
import { AnswerObject } from '../Types';
|
||||||
import { Capitalize } from '../Utils';
|
import { unEscape } from '../Utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
answers: string[];
|
answers: string[];
|
||||||
callback: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
callback: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
difficulty: string;
|
|
||||||
question: string;
|
question: string;
|
||||||
category: string;
|
userAnswers: AnswerObject | undefined;
|
||||||
questionNr: number;
|
|
||||||
score: number;
|
|
||||||
totalQuestions: number;
|
|
||||||
userAnswer: AnswerObject | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const QuestionCard: React.FC<Props> = ({
|
const QuestionCard: React.FC<Props> = ({
|
||||||
answers,
|
answers,
|
||||||
callback,
|
callback,
|
||||||
difficulty,
|
|
||||||
question,
|
question,
|
||||||
category,
|
userAnswers
|
||||||
questionNr,
|
|
||||||
score,
|
|
||||||
totalQuestions,
|
|
||||||
userAnswer
|
|
||||||
}) => (
|
}) => (
|
||||||
<div className="question-card-wrapper">
|
<div className="question-card-wrapper">
|
||||||
<div className="details">
|
<p className="question">{unEscape(question)}</p>
|
||||||
<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>
|
<div>
|
||||||
{answers.map((answer) => (
|
{answers.map((answer) => (
|
||||||
<ButtonWrapper
|
<ButtonWrapper
|
||||||
correct={userAnswer?.correctAnswer === answer}
|
correct={userAnswers?.correctAnswer === answer}
|
||||||
userClicked={userAnswer?.answer === answer}
|
userClicked={userAnswers?.answer === answer}
|
||||||
key={answer}
|
key={answer}
|
||||||
>
|
>
|
||||||
<button disabled={!!userAnswer} onClick={callback} value={answer}>
|
<button disabled={!!userAnswers} onClick={callback} value={answer}>
|
||||||
<span dangerouslySetInnerHTML={{ __html: answer }} />
|
<span>{unEscape(answer)}</span>
|
||||||
</button>
|
</button>
|
||||||
</ButtonWrapper>
|
</ButtonWrapper>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ import { SelectWrapper } from '../App.style';
|
|||||||
import { Category } from '../Types';
|
import { Category } from '../Types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
category: number;
|
categoryID: number;
|
||||||
categoryList: Array<Category>;
|
categoryList: Array<Category>;
|
||||||
setCategory: (category: number) => void;
|
setCategoryID: (categoryID: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectCategory = ({ category, categoryList, setCategory }: Props) => {
|
const SelectCategory = ({ categoryID, categoryList, setCategoryID }: Props) => {
|
||||||
return (
|
return (
|
||||||
<SelectWrapper
|
<SelectWrapper
|
||||||
value={category}
|
value={categoryID}
|
||||||
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setCategory(e.target.value as number);
|
setCategoryID(e.target.value as number);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value={1}>Any Category</option>
|
<option value={1}>Random Category</option>
|
||||||
{categoryList.map((cat) => (
|
{categoryList.map((category) => (
|
||||||
<option value={cat.id} key={cat.id}>
|
<option value={category.id} key={category.id}>
|
||||||
{cat.name}
|
{category.name.replace(/Entertainment: |Science: /g, '')}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</SelectWrapper>
|
</SelectWrapper>
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
);
|
);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user