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;
|
||||
margin-bottom: 2rem;
|
||||
@media (max-width: 768px) {
|
||||
font-size: 2rem;
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,13 +51,14 @@ export const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.details {
|
||||
margin: 2rem 0;
|
||||
margin: .3rem 0;
|
||||
font-size: 1.4rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ export const ButtonWrapper = styled.div<ButtonWrapperProps>`
|
||||
export const SelectWrapper = styled.select`
|
||||
width: 100%;
|
||||
max-width: 70rem;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
background-color: #22363c;
|
||||
color: white;
|
||||
padding: 0.24rem;
|
||||
|
||||
+35
-29
@@ -5,25 +5,26 @@ import { GlobalStyle, Wrapper } from './App.style';
|
||||
import SelectDifficulty, { Difficulty } from './components/SelectDifficulty';
|
||||
import SelectQuestions, { TotalQuestions } from './components/SelectQuestions';
|
||||
import SelectCategory from './components/SelectCategory';
|
||||
import DetailsCard from './components/DetailsCard';
|
||||
import { Category, QuestionState, AnswerObject } from './Types';
|
||||
|
||||
const App = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [questions, setQuestions] = useState<QuestionState[]>([]);
|
||||
const [number, setNumber] = useState(0);
|
||||
const [questionNumber, setQuestionNumber] = useState(0);
|
||||
const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([]);
|
||||
const [score, setScore] = useState(0);
|
||||
const [gameOver, setGameOver] = useState(true);
|
||||
const [gameOver, setGameOver] = useState<boolean>(true);
|
||||
const [difficulty, setDifficulty] = useState<string>(Difficulty.EASY);
|
||||
const [totalQuestionNumber, setTotalQuestionNumber] = useState<number>(
|
||||
TotalQuestions.TEN
|
||||
);
|
||||
|
||||
const [category, setCategory] = useState<number>(1);
|
||||
const [categoryID, setCategoryID] = useState<number>(1);
|
||||
const [categoryList, setCategoryList] = useState<Array<Category>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories().then((data) => setCategoryList(data));
|
||||
fetchCategories().then((categories) => setCategoryList(categories));
|
||||
}, []);
|
||||
|
||||
const startTrivia = async () => {
|
||||
@@ -32,14 +33,14 @@ const App = () => {
|
||||
|
||||
const newQuestions = await fetchQuizQuestions(
|
||||
totalQuestionNumber,
|
||||
category,
|
||||
categoryID,
|
||||
difficulty
|
||||
);
|
||||
|
||||
setQuestions(newQuestions);
|
||||
setScore(0);
|
||||
setUserAnswers([]);
|
||||
setNumber(0);
|
||||
setQuestionNumber(0);
|
||||
setQuestions(newQuestions);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -47,17 +48,16 @@ const App = () => {
|
||||
if (!gameOver) {
|
||||
const answer = e.currentTarget.value;
|
||||
|
||||
const correct = questions[number].correct_answer === answer;
|
||||
|
||||
const correct = questions[questionNumber].correct_answer === answer;
|
||||
if (correct) {
|
||||
setScore((prev) => prev + 1);
|
||||
}
|
||||
|
||||
const answerObject = {
|
||||
question: questions[number].question,
|
||||
question: questions[questionNumber].question,
|
||||
answer,
|
||||
correct,
|
||||
correctAnswer: questions[number].correct_answer
|
||||
correctAnswer: questions[questionNumber].correct_answer
|
||||
};
|
||||
|
||||
setUserAnswers((prev) => [...prev, answerObject]);
|
||||
@@ -65,11 +65,10 @@ const App = () => {
|
||||
};
|
||||
|
||||
const nextQuestion = () => {
|
||||
const nextNumber = number + 1;
|
||||
if (nextNumber === totalQuestionNumber) {
|
||||
if (questionNumber + 1 >= totalQuestionNumber) {
|
||||
setGameOver(true);
|
||||
} else {
|
||||
setNumber(nextNumber);
|
||||
setQuestionNumber(questionNumber + 1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,7 +78,7 @@ const App = () => {
|
||||
<Wrapper className="App">
|
||||
<h1>Quiz</h1>
|
||||
|
||||
{gameOver || number + 1 === totalQuestionNumber ? (
|
||||
{gameOver || questionNumber === totalQuestionNumber ? (
|
||||
<div>
|
||||
<SelectDifficulty
|
||||
difficulty={difficulty}
|
||||
@@ -90,37 +89,44 @@ const App = () => {
|
||||
setTotalQuestionNumber={setTotalQuestionNumber}
|
||||
/>
|
||||
<SelectCategory
|
||||
category={category}
|
||||
categoryID={categoryID}
|
||||
categoryList={categoryList}
|
||||
setCategory={setCategory}
|
||||
setCategoryID={setCategoryID}
|
||||
/>
|
||||
<button className="start" onClick={startTrivia}>
|
||||
Start
|
||||
{number + 1 === totalQuestionNumber && ' again'}
|
||||
{questionNumber + 1 === totalQuestionNumber && ' again'}
|
||||
</button>
|
||||
</div>
|
||||
) : 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 && (
|
||||
<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}
|
||||
question={questions[questionNumber].question}
|
||||
answers={questions[questionNumber].answers}
|
||||
userAnswers={userAnswers ? userAnswers[questionNumber] : undefined}
|
||||
callback={checkAnswer}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!gameOver &&
|
||||
!loading &&
|
||||
userAnswers.length === number + 1 &&
|
||||
number + 1 !== totalQuestionNumber ? (
|
||||
userAnswers.length === questionNumber + 1 &&
|
||||
questionNumber !== totalQuestionNumber ? (
|
||||
<button className="next" onClick={nextQuestion}>
|
||||
Next Question
|
||||
</button>
|
||||
|
||||
@@ -9,3 +9,18 @@ export const Capitalize = (text: string) => {
|
||||
(_, 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 { ButtonWrapper } from '../App.style';
|
||||
import { AnswerObject } from '../Types';
|
||||
import { Capitalize } from '../Utils';
|
||||
import { unEscape } 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;
|
||||
userAnswers: AnswerObject | undefined;
|
||||
};
|
||||
|
||||
const QuestionCard: React.FC<Props> = ({
|
||||
answers,
|
||||
callback,
|
||||
difficulty,
|
||||
question,
|
||||
category,
|
||||
questionNr,
|
||||
score,
|
||||
totalQuestions,
|
||||
userAnswer
|
||||
userAnswers
|
||||
}) => (
|
||||
<div className="question-card-wrapper">
|
||||
<div className="details">
|
||||
<div>Difficulty: {Capitalize(difficulty)}</div>
|
||||
<div>{category}</div>
|
||||
<p className="question">{unEscape(question)}</p>
|
||||
<div>
|
||||
{answers.map((answer) => (
|
||||
<ButtonWrapper
|
||||
correct={userAnswers?.correctAnswer === answer}
|
||||
userClicked={userAnswers?.answer === answer}
|
||||
key={answer}
|
||||
>
|
||||
<button disabled={!!userAnswers} onClick={callback} value={answer}>
|
||||
<span>{unEscape(answer)}</span>
|
||||
</button>
|
||||
</ButtonWrapper>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,24 +4,24 @@ import { SelectWrapper } from '../App.style';
|
||||
import { Category } from '../Types';
|
||||
|
||||
type Props = {
|
||||
category: number;
|
||||
categoryID: number;
|
||||
categoryList: Array<Category>;
|
||||
setCategory: (category: number) => void;
|
||||
setCategoryID: (categoryID: number) => void;
|
||||
};
|
||||
|
||||
const SelectCategory = ({ category, categoryList, setCategory }: Props) => {
|
||||
const SelectCategory = ({ categoryID, categoryList, setCategoryID }: Props) => {
|
||||
return (
|
||||
<SelectWrapper
|
||||
value={category}
|
||||
value={categoryID}
|
||||
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
||||
e.preventDefault();
|
||||
setCategory(e.target.value as number);
|
||||
setCategoryID(e.target.value as number);
|
||||
}}
|
||||
>
|
||||
<option value={1}>Any Category</option>
|
||||
{categoryList.map((cat) => (
|
||||
<option value={cat.id} key={cat.id}>
|
||||
{cat.name}
|
||||
<option value={1}>Random Category</option>
|
||||
{categoryList.map((category) => (
|
||||
<option value={category.id} key={category.id}>
|
||||
{category.name.replace(/Entertainment: |Science: /g, '')}
|
||||
</option>
|
||||
))}
|
||||
</SelectWrapper>
|
||||
|
||||
+4
-1
@@ -2,12 +2,15 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user