React Quiz Tutorial

Coaspe·2021년 1월 30일
0

React_Practice

목록 보기
1/2

React Quiz app Tutorial
위 영상을 복습하는 글이다.

QuestionCard.tsx

import React from 'react';
import {AnswerObject} from '../App';
import {Wrapper, ButtonWrapper} from './QuestionCard.styles';
** styled-components import **

type Props = {
    question: string;
    answers: string[];
    callback: (e: React.MouseEvent<HTMLButtonElement>) => void;
	** e는 MouseEvent중에서도 HTMLButtonElement 타입이다. **
    userAnswer: AnswerObject | undefined;
    questionNr: number;
    totalQuestions: number;
}
** QuestionCard component로 넘겨줄 Props의 타입을 정의한다. **

const QuestionCard: React.FC<Props> = ({
  			** Specifing props **
    question, 
    answers, 
    callback, 
    userAnswer, 
    questionNr, 
    totalQuestions
    }) => (
    <Wrapper>
        <p className="number">
            Question:{questionNr} / {totalQuestions}
        </p>
        <p dangerouslySetInnerHTML={{__html: question}} />
        <div>
        {answers.map((answer) => (
            <ButtonWrapper 
                key={answer}
                correct={userAnswer?.correctAnswer === answer}
                // optinal chaining
                userClicked={userAnswer?.answer === answer}>

          <button disabled={userAnswer ? true : false} value={answer} onClick={callback}>
            <span dangerouslySetInnerHTML={{ __html: answer }} />
		**dangerouslySetInnerHTML InnerHTML과 같은 것이다.**
          </button>
            </ButtonWrapper>
        ))}
        </div>
    </Wrapper>
);

export default QuestionCard;

QuestionCard.styles.ts

import styled from 'styled-components';

export const Wrapper = styled.div`
    max-width: 1100px;
    background: #ebfeff;
    border-radius:10px;
    border: 2px solid #0085a3;
    padding: 20px;
    box-shadow: 0px 5px 10px rgba(0,0,0,0.25);
    text-align: center;

    p {
        font-size: 1rem;
    }
`

type ButtonWrapperProps = {
    correct: boolean;
    userClicked: boolean;
}
**ButtonWrapper styled-component로 전달해줄 Props의 type이다.**
export const ButtonWrapper = styled.div<ButtonWrapperProps>`
    transition: all 0.3 ease;

    :hover {
        opacity: 0.8;
    }

    button {
        cursor: pointer;
        user-select: none;
        font-size: 0.8rem;
        width: 100%;
        height: 40px;
        margin: 5px 0;
        background: ${({correct, userClicked}) =>
            correct
                ? 'linear-gradient(90deg, #56ffa4, #59bc86)'
                : !correct && userClicked
                ? 'linear-gradient(90deg, #ff5656, #c16868)'
                : 'linear-gradient(90deg, #56ccff, #6eafb4)'};
	**correct,userClicked의 상태에 따라 색이 달라진다.**
        border: 3px solid #fff;
        box-shadow: 1px 2px 0px rgba(0, 0, 0, 0.1);
        border-radius: 10px;
        color: #fff;
        text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.25);
    }
`;

API.tsx

import { shuffleArray } from './utils';

export type Question = {
  category: string;
  correct_answer: string;
  difficulty: string;
  incorrect_answers: string[];
  question: string;
  type: string;
};
**JSON 형태로 받아온 데이터의 type을 정의한다.**

export enum Difficulty {
  EASY = "easy",
  MEDIUM = "medium",
  HARD = "hard",
}

export type QuestionsState = Question & { answers: string[] };
**&을 활용해 type을 정의한다.**

export const fetchQuizQuestions = async (amount: number, difficulty: Difficulty): Promise<QuestionsState[]> => {
  const endpoint = `https://opentdb.com/api.php?amount=${amount}&difficulty=${difficulty}&type=multiple`;
  const data = await (await fetch(endpoint)).json();
  return data.results.map((question: Question) => ({
    ...question,
    answers: shuffleArray([...question.incorrect_answers, question.correct_answer])
  }))
};

data.result(Question[])

question(Question)

utils.ts

export const shuffleArray = (array: any[]) =>
    [...array].sort(() => Math.random() - 0.5);

App.tsx

import React ,{useState} from 'react';
import QuestionCard from './components/QuestionCard';
import {fetchQuizQuestions, QuestionsState, Difficulty} from './API';
import {GlobalStyle, Wrapper} from './App.styles';

export type AnswerObject = {
  question: string;
  answer: string;
  correct: boolean;
  correctAnswer: string;
}
**정답의 type을 정의한다.**
const TOTAL_QUESTIONS = 10;
**질문의 총개수이다.**
const App = () => {
  const [loading, setLoading] = useState(false);
	**loading상태를 나타낸다.**
  const [questions, setQuestions] = useState<QuestionsState[]>([]);
	**<QuestionState[]>는 questions의 타입을 specifing 한다.**
  const [number, setNumber] = useState(0);
  const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([]);
  const [score, setScore] = useState(0);
  const [gameOver, setGameOver] = useState(true);

  console.log(questions);

  const startTrivia = async () => {
    setLoading(true);
    setGameOver(false);

    const newQuestions = await fetchQuizQuestions(
      TOTAL_QUESTIONS,
      Difficulty.EASY
    );

    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);
      //Save answer in the array for user answers
      const answerObject = {
        question: questions[number].question,
        answer, // answer: answer
        correct,
        correctAnswer: questions[number].correct_answer,
      };
      setUserAnswers(prev => [...prev, answerObject]);
    }
  }

  const nextQuestion = () => {
    const nextQuestion = number + 1;
    if(nextQuestion === TOTAL_QUESTIONS){
      setGameOver(true);
    } else {
      setNumber(nextQuestion);
    }
  }
  return (
    <>
    <GlobalStyle />
    <Wrapper className="App">
      <h1>REACT QUIZ</h1>
      {gameOver || userAnswers.length === TOTAL_QUESTIONS ? ( // ||는 or을 의미한다.
      <button className="start" onClick={startTrivia}>
        Start
      </button>
  ) : null}
      {!gameOver ? <p className="score">Score: {score}</p> : null}
      {loading ? <p>Loading Question ...</p> : null}
      {!loading && !gameOver && (
       <QuestionCard  
        questionNr={number + 1}
        totalQuestions={TOTAL_QUESTIONS}
        question={questions[number].question}
        answers={questions[number].answers}
        userAnswer={userAnswers ? userAnswers[number] : undefined}
        callback={checkAnswer}
      />
      )}
      {!gameOver && !loading && userAnswers.length === number + 1 && number !== TOTAL_QUESTIONS - 1 ? (
              <button className="next" onClick={nextQuestion}>
              Next Question
            </button>
      ) : null}
    </Wrapper>
    </>
  );
};

export default App;

App.styles.ts

import styled, {createGlobalStyle} from 'styled-components';
import BGImage from './images/aditya-chinchure-YfAn4arFq74-unsplash.jpg';

export const GlobalStyle = createGlobalStyle`
**app전체에 적용할 스타일이다.**
    html {
        height : 100%;
    }

    body {
        background-image:url(${BGImage});
        background-size: cover;
        margin: 0;
        padding: 0 20px;
        display: flex;
        justify-content: center;
    }

    * {
        box-sizing: border-box;
        font-family: 'Catamaran', sans-serif;
    }
`;

export const Wrapper = styled.div`
    display: flex;
    flex-direction: column;
    align-items: center;

> p {
        color: #fff;
    }
** forward arrow 모든 p tag에 스타일링 되는 것을 피하기 위해서 사용한다. **

    .score {
        color:#fff;
        font-size: 2rem;
        margin:0;
    }

    h1 {
        font-family: Fascinate Inline , Haettenschweiler, 'Arial Narrow Bold', sans-serif;
        background-image: linear-gradient(180deg, #fff, #87f1ff);
        background-size: 100%;
        background-clip: text;
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        -moz-background--clip:text;
        -moz-text-fill-color: transparent;
        filter: drop-shadow(2px 2px #0085a3);
	**흐림 효과나 색상 변형 등 그래픽 효과를 요소에 적용한다**
        font-size: 70px;
        text-align: center;
        font-weight: 400;
        margin: 20px;
    }

    .start, .next {
        cursor: pointer;
        background: linear-gradient(190deg, #fff, #ffcc91);
        border: 2px solid #d38558;
        box-shadow: 0px 5px 10px rgba(0,0,0,0.25);
        border-radius: 10px;
        margin: 20px 0;
        padding: 0 40px;
    }

    .start {
        max-width: 200px;
    }
`
profile
https://github.com/Coaspe

0개의 댓글