4
0

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 3 years have passed since last update.

Nuxt.js で国別クイズを作成してみた!

Last updated at Posted at 2021-03-27

Vue.jsで国別クイズを作成してみた。

はじめに

最近DevChallenges とういうサービスを知りました。
DevChallengesは、いろんなWeb開発課題が用意されており、スキルアップを図るサイトです。
おもしろそうなものがたくさんあり、迷ったのですが、今回はCountryQuizというものを試してみました!
image.png

完成品

Country-Quiz

要件

  • 1番初めに挑戦するクイズの種類を選べます(国名から6州を当てるクイズ・国旗から国を当てるクイズ)

  • クイズは毎回答後に正誤が表示される

  • 正解数はカウントされ、最後に結果発表画面を見られる

  • クイズは再挑戦できる

使用したもの

APIは REST COUNTRIES を使用しました
フレームワークはNuxt.jsを使用しました。
明らかにオーバースペックなのですが、これから認証機能や、クイズ結果を記録していき、マイページで表示などを実装したいなと思ったのでNuxtにしました!

実装したソースコード

<template>
  <div id="index">
    <div class="auth-box">
      <div v-if="loginUser">
        <a class="logout" @click="logout">ログアウト</a>
      </div>
      <a v-else class="login" @click.prevent="login">ログイン</a>
    </div>
    <div class="index-inner">
      <h1 class="title is-1">COUNTRY QUIZ</h1>
      <img
        v-if="status <= endNum"
        class="decoration"
        src="/images/def.svg"
        alt=""
      />
      <div class="quiz-box">
        <div class="quiz-box-inner">
          <transition mode="out-in">
            <!-- 初期表示 -->
            <div v-if="status === 0" key="init" class="init">
              <h2 class="question title is-2">
                {{ displayQuiz.question }}
              </h2>
              <div class="answers">
                <button
                  v-for="option in displayQuiz.options"
                  :key="option.name"
                  class="button is-medium is-fullwidth"
                  @click="startQuiz(option.name)"
                >
                  {{ option.jaName }}
                </button>
              </div>
            </div>
            <!-- クイズ終了表示 -->
            <div v-else-if="status > endNum" key="finish" class="finished">
              <img src="/images/finish.svg" alt="" />
              <h2 class="question title">FINISH</h2>
              <p class="result">
                あなたは5問中
                <span class="correct-answers">{{ correctAnswers }}</span>
                問正解しました
              </p>
              <button
                class="button is-medium is-fullwidth try-again"
                @click="backMenu"
              >
                メニューに戻る
              </button>
            </div>
            <!-- 質問表示 -->
            <div v-else key="quiz" class="in-progress">
              <div class="progress-wrapper">
                <div ref="meter" class="progress-bar"></div>
              </div>
              <!-- 国旗クイズの場合のみ表示される -->
              <img
                v-if="displayQuiz.flagImageUrl"
                :src="displayQuiz.flagImageUrl"
                class="flag-img"
                alt=""
              />
              <h2 class="question title is-2">
                {{ displayQuiz.question }}
              </h2>
              <div ref="answers" class="answers">
                <button
                  v-for="(option, index) in displayQuiz.options"
                  :key="index"
                  class="button is-medium is-fullwidth"
                  :class="{
                    correct: isExamining && displayQuiz.answer == option.name,
                  }"
                  @click="answer(option.name, $event)"
                >
                  {{ option.jaName }}
                </button>
                <button
                  v-if="isExamining"
                  class="next-btn button"
                  @click="continueQuiz"
                >
                  {{ nextBtnText }}
                </button>
                <!-- 回答後はNEXT以外触れなくします -->
                <div v-if="isExamining" class="touch-prevention"></div>
              </div>
            </div>
          </transition>
        </div>
      </div>
    </div>
    <pre>{{ loginUser }}</pre>
  </div>
</template>

<script>
import axios from 'axios'
import { mapGetters, mapActions } from 'vuex'

const DATA_MAX = 200 // APIのデータ上限
const regionOptions = [
  {
    name: 'Asia',
    jaName: 'アジア州',
  },
  {
    name: 'Africa',
    jaName: 'アフリカ州',
  },
  {
    name: 'Americas',
    jaName: 'アメリカ州',
  },
  {
    name: 'Oceania',
    jaName: 'オセアニア州',
  },
  {
    name: 'Europe',
    jaName: 'ヨーロッパ州',
  },
]

function genRandomArray(array, max) {
  while (true) {
    const randomNum = Math.floor(Math.random() * max)
    if (!array.includes(randomNum)) {
      array.push(randomNum)
      break
    }
  }
}

export default {
  data() {
    return {
      quizzes: [],
      endNum: 5,
      status: 0,
      correctAnswers: 0,
      isExamining: false,
    }
  },
  computed: {
    ...mapGetters(['loginUser']),
    displayQuiz() {
      return this.quizzes[this.status]
    },
    nextBtnText() {
      if (this.status === this.endNum) return '結果を見る'
      else return '次へ'
    },
  },
  created() {
    // 初期表示の設問と選択肢
    this.initQuizzes()
  },
  methods: {
    ...mapActions(['login', 'logout']),
    // クイズを進行していくためのメソッド
    continueQuiz() {
      // 選択した回答のハイライト用classをはずす
      const all = this.$refs.answers.querySelectorAll('button')
      console.log(all)
      all.forEach((el) => {
        el.classList.remove('user-answered')
      })

      // クイズを進める
      this.status++
      this.isExamining = false
    },
    // 回答後に正誤表示を行うメソッド
    examiningAnswer() {
      this.isExamining = true
    },

    // クイズをリセットしてメニューに戻るメソッド
    backMenu() {
      this.status = 0
      this.correctAnswers = 0
      this.initQuizzes()
    },
    // 通信を飛ばして、クイズ作成へ続けるメソッド
    startQuiz(type) {
      axios
        .get('https://restcountries.eu/rest/v2/all')
        .then((res) => {
          res.data = res.data.filter((el) => {
            return el.translations.ja !== null
          })
          if (type === 'region') this.makeRegionQuiz(res.data)
          else if (type === 'flag') this.makeFlagQuiz(res.data)
          this.status = 1
          this.correctAnswers = 0
        })
        .catch((error) => {
          throw new Error(error)
        })
    },

    // 6州クイズを生成するメソッド
    makeRegionQuiz(responseData) {
      const randomNumArray = []
      for (let n = 0; n < 5; n++) {
        genRandomArray(randomNumArray, DATA_MAX)
      }
      console.log(randomNumArray)
      for (let i = 0; i < this.endNum; i++) {
        const country = responseData[randomNumArray[i]]
        const quiz = {
          question: `${country.translations.ja}が属する州はどこですか?`,
          options: regionOptions,
          answer: country.region,
        }
        this.quizzes.push(quiz)
      }
    },
    // 国旗クイズを生成するメソッド
    makeFlagQuiz(responseData) {
      const randomNumArray = []
      for (let n = 0; n < 5; n++) {
        genRandomArray(randomNumArray, DATA_MAX)
      }
      for (let i = 0; i < this.endNum; i++) {
        // 正解を作成
        const country = responseData[randomNumArray[i]]
        const flagOptions = []
        const flagOption = {
          name: country.name,
          jaName: country.translations.ja,
        }
        // 正解セット
        flagOptions.push(flagOption)

        // 不正解問題作成
        const randomNumArray2 = []
        for (let j = 0; j < 4; j++) {
          while (true) {
            const randomNum = Math.floor(Math.random() * DATA_MAX)
            if (
              !randomNumArray2.includes(randomNum) &&
              randomNum !== randomNumArray[i] // 正解をかぶってしまうといけないので&&以下の記述が必要
            ) {
              randomNumArray2.push(randomNum)
              break
            }
          }
          // 不正解問題をセット
          flagOptions.push({
            name: responseData[randomNumArray2[j]].name,
            jaName: responseData[randomNumArray2[j]].translations.ja,
          })
        }
        // 正解が先頭でセットされてしまっているのでsortをかけて分からなくする
        flagOptions.sort((a, b) => {
          if (a.name > b.name) return -1
          else if (a.name < b.name) return 1
          else return 0
        })
        const quiz = {
          flagImageUrl: country.flag,
          question: `この国旗はどこの国のものですか?`,
          options: flagOptions,
          answer: country.name,
        }
        this.quizzes.push(quiz)
      }
    },
    // ユーザーがクイズに回答したときのメソッド
    answer(userAnswer, e) {
      // 選択した答えをハイライトさせる処理
      e.target.classList.add('user-answered')
      // 正解したらカウントして正誤表示を行う
      if (userAnswer === this.displayQuiz.answer) this.correctAnswers++
      this.examiningAnswer()
      this.advanceMeter()
    },
    // クイズの進捗メーターを進めるメソッド
    advanceMeter() {
      const meter = this.$refs.meter
      let percent = 0
      percent += (this.status / this.endNum) * 100
      meter.style.width = percent + '%'
    },
    // quizzesの初期化
    initQuizzes() {
      this.quizzes = []
      const initialQuestions = {
        question: 'どの知識についてクイズをしたいですか?',
        options: [
          {
            name: 'region',
            jaName: '地域区分クイズ',
          },
          {
            name: 'flag',
            jaName: '国旗クイズ',
          },
        ],
      }
      this.quizzes.push(initialQuestions)
    },
  },
}
</script>

おわりに

書いてみた感想は、まだ少し共通化できそうな部分があるなぁという感じです。(クイズ生成あたり)
これから、Firebaseの認証機能とCloudFireStoreを使い、ログインしているユーザーのクイズ結果は記録していけるようにしていこうと思います。
DevChallengesは他にもまだまだ開発課題があるので、当分退屈しなさそうで嬉しいですねー!

4
0
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
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?