3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Next】Next.jsでクイズアプリを作る

Posted at

Next.jsでクイズアプリを作っていきます。Typescriptも使用していきます。バージョンによってエラーが起きる可能性がありので下記のバージョンを確認してください。

  • next@12.1.0
  • styled-components@5.3.3
  • typescript@4.6.2

next_quiz.png

参考動画

こちらの動画はReactでクイズ作ってます。Reactで作りたい場合はこちらの動画を見てください。またこの動画では画像を使用していますがここでは使用せずcssを使っていきます。

外部API

QuizAPI

こちらのAPIはAPIキーがいらず登録もしなくていいので簡単に使用できます。

では早速作っていきましょう。

yarn create next-app --typescript

中身はこんな感じにしていきます。またアプリを作るときstylesは省略していきますので詳しくはコードを確認してください。

.
┝ node_modules
┝ public
┝ src
│ ┝ components
│  ┝ api
│  ┝ Questions
│  ┝ utils
│ ┝ pages
│ ┝ styles
┝ .eslintrc.json
┝ .gitignore
┝ next-nev.d.ts
┝ next.config.js
┝ README.md
┝ package.json
┝ tsconfig.json
└ yarn.lock

インストール

yarn add styled-components

yarn add -D @types/styled-components

最初にtscofig.jsoxを少し変えていきます。

tscofig.jsox
{
  "compilerOptions": {
    "baseUrl": "src",
  }, 
}

これで../を減らせます。

  • _app.tsx
pages/_app.tsx
import 'styles/globals.css'
import Layout from 'components/Layout'
import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

export default MyApp
  • _document.tsx
pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'

const MyDocument = () => {
  return (
    <Html lang="ja-JP">
      <Head>
        <meta name="application-name" content="MyTemplate" />
        <meta name="description" content="" />
        <link
          href="https://fonts.googleapis.com/css2?family=Catamaran:wght@700&family=Fascinate+Inline&display=swap"
          rel="stylesheet"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

export default MyDocument


  • Layout.tsx
components/Layout.tsx
import Head from 'next/head'

import { ReactNode } from 'react'

type Props = {
  children?: ReactNode
}

const Layout = ({ children }: Props) => {
  return (
    <div>
      <Head>
        <title>MyTemplate</title>
      </Head>

      <div className="content">{children}</div>

      <footer className=""></footer>
    </div>
  )
}

export default Layout
  • index.tsx
pages/index.tsx
import type { NextPage } from 'next'
import QuestionCard from 'components/Questions/QuestionCard'
import styles from 'styles/Home.module.css'

const Home: NextPage= () => {

 const startTrivia = async () => {}

 const nextQuestion = () => {}

 const checkAnswer = (e: any) => {}

 return (
  <div className={styles.container}>
   <h1>Next.js quiz</h1>
   <button className={styles.start}>
          start
   </button>
   <p className={styles.score}>Score:{score}</p>
   <p>Loading Questons...</p>
// <QuestionCard/>
   <button>Next Question</button>
  </div>
 )
}

export default Home

QuestionCard.tsx

components/Questions/QuestionCard.tsx
import styles from 'styles/QuestionCard.module.css'
import { ButtonWrapper } from './styled'

const QuestionCard = () => {
 return (
  <div className={styles.container}>
   <p>Question: </p>
   <p/>
   <div>
    <ButtonWrapper>
      <button>
         <span/>
       </button>
     </ButtonWrapper>
   </div>
  </div>
 )
}

cssやstyledの中身はコードを確認してコピペしてください。

localhostを確認できましたか?

  • utiles.ts
    これはクイズをランダムに表示させるための必要なものです。
components/utiles/utiles.ts
export const shuffleArray = (array: any[]) =>
  [...array].sort(() => Math.random() - 0.5)

utilesはAPIに持っていきます。

  • API.ts
components/api/API.ts
import { shuffleArray } from '../utils/utils'

export type Question = {
  category: string
  correct_answer: string
  difficulty: string
  incorrect_answers: string[]
  question: string
  type: string
}

// 難易度
export enum Difficulty {
  EASY = 'easy',
  MEDIUM = 'medium',
  HARD = 'hard',
}

export type QuestionsState = Question & { answers: string[] }



export const fetchQuizQuestions = async (
  amount: number,
  difficulty: Difficulty,
): Promise<QuestionsState[]> => {

// amount はクイズの問題を表示する数を入れます。数の設定はindex.tsx でします。
// difficulty は難易度これも設定はindex.tsx でします。
// type=multiple は複数の種類の問題

  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,
// ここでshuffleArrayでランダムに問題を表示させます
    answers: shuffleArray([
      ...question.incorrect_answers,
      question.correct_answer,
    ]),
  }))
}

Open Trivia DB では難易度、数、問題のタイプなど色んな組み合わせができますAPIで設定できるので、是非自分で設定してみてください。

ではindex.tsxでスタート時の動きを書いていきます

  • index.tsx
pages/index.tsx
import { useState } from 'react'
import { fetchQuizQuestions } from '../components/api/API'
            :
// types
import { QuestionsState, Difficulty } from '../components/api/API'

export type AnswerObject = {
  question: string
  answer: string
  correct: boolean
  correctAnswer: string
}

// クイズの出題数
const TOTAL_QUESTIONS = 10

const Home: NextPage = () => {
  const [loading, setLoading] = useState(false)
  const [questions, setQuestions] = useState<QuestionsState[]>([])
  const [number, setNumber] = useState(0)
  const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([])
  const [score, setScore] = useState(0)
  const [gameOver, setGameOver] = useState(true)

  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 nextQuestion = () => {}

  const checkAnswer = (e: any) => {}

  return (
    <div className={styles.container}>
      <h1>Next.js quiz</h1>
       {gameOver || userAnswers.length === TOTAL_QUESTIONS ? (
        <button className={styles.start} onClick={startTrivia}>
          start
        </button>
      ) : null}
// !gameOverで間違いたらスコアをカウントしない
       {!gameOver ? <p className={styles.score}>Score:{score}</p> : null}
       {loading ? <p>Loading Questons...</p> : null}

        :
    </div>
  )
}

startTrivia ではstartボタンを押したときにクイズのデータを持ってきてsetScoreで0から始めsetNumberでカウントしていきます。最後にsetLoading(false)にしましょう。

checkAnswer の中身を書いていきます。

pages/index.tsx

const checkAnswer = (e: any) => {
    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])
    }
  }

 return( 
              :
              :
        
      {!loading && !gameOver && (
        <QuestionCard
          questionNr={number + 1}
          totalQuestions={TOTAL_QUESTIONS}
          question={questions[number].question}
          answers={questions[number].answers}
          userAnswer={userAnswers ? userAnswers[number] : undefined}
          callback={checkAnswer}
        />
      )}
 )

checkAnswer の変数は QuestionCard に渡します。

nextQuestion を書いていきます。

pages/index.tsx
const nextQuestion = () => {
    // 10問までNext Questionボタンを押せるようにします。
    const nextQ = number + 1

    if (nextQ === TOTAL_QUESTIONS) {
      setGameOver(true)
    } else {
      setNumber(nextQ)
    }
  }

 return (
              :
              :


     {!gameOver &&
      !loading &&
      userAnswers.length === number + 1 &&
      number !== TOTAL_QUESTIONS - 1 ? (
        <button onClick={nextQuestion}>Next Question</button>
      ) : null}
 )
index.tsx全体のコード
index.tsx
import { useState } from 'react'
import type { NextPage } from 'next'
import { fetchQuizQuestions } from '../components/api/API'
// components
import QuestionCard from 'components/Questions/QuestionCard'
// styles
import styles from 'styles/Home.module.css'
// types
import { QuestionsState, Difficulty } from '../components/api/API'

export type AnswerObject = {
  question: string
  answer: string
  correct: boolean
  correctAnswer: string
}

const TOTAL_QUESTIONS = 10

const Home: NextPage = () => {
  const [loading, setLoading] = useState(false)
  const [questions, setQuestions] = useState<QuestionsState[]>([])
  const [number, setNumber] = useState(0)
  const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([])
  const [score, setScore] = useState(0)
  const [gameOver, setGameOver] = useState(true)

  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: any) => {
    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 nextQ = number + 1

    if (nextQ === TOTAL_QUESTIONS) {
      setGameOver(true)
    } else {
      setNumber(nextQ)
    }
  }

  return (
    <div className={styles.container}>
      <h1>Next.js quiz</h1>
      {gameOver || userAnswers.length === TOTAL_QUESTIONS ? (
        <button className={styles.start} onClick={startTrivia}>
          start
        </button>
      ) : null}
      {!gameOver ? <p className={styles.score}>Score:{score}</p> : null}
      {loading ? <p>Loading Questons...</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 onClick={nextQuestion}>Next Question</button>
      ) : null}
    </div>
  )
}

export default Home

QuestionCard を作れば完成です。

components/Questions/QuestionCard.tsx

import { AnswerObject } from 'pages/index'
// styles
import styles from 'styles/QuestionCard.module.css'
import { ButtonWrapper } from './styled'

type Props = {
  question: string
  answers: string[]
  callback: (e: React.MouseEvent<HTMLButtonElement>) => void
  userAnswer: AnswerObject | undefined
  questionNr: number
  totalQuestions: number
}

const QuestionCard = ({
  question,
  answers,
  callback,
  userAnswer,
  questionNr,
  totalQuestions,
}: Props) => {
  return (
    <div className={styles.container}>
      <p>
        {' '}
        Question: {questionNr} / {totalQuestions}
      </p>
      <p dangerouslySetInnerHTML={{ __html: question }} />
      <div>
        {answers.map((answer) => (
          <ButtonWrapper
            key={answer}
            correct={userAnswer?.correctAnswer === answer}
            userClicked={userAnswer?.answer === answer}
          >
            <button
              disabled={userAnswer ? true : false}
              value={answer}
              onClick={callback}
            >
              <span dangerouslySetInnerHTML={{ __html: answer }} />
            </button>
          </ButtonWrapper>
        ))}
      </div>
    </div>
  )
}

export default QuestionCard

http://localhost:3000 を開いて確かめてください。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?