因為今天已經來到倒數第二天!
覺得應該要把之前介紹過的東西全部融合在一起展現出來,
也算是成果展吧XD
今天要用 Next.js 搭配 Bootstrap + React + Reactstrap,
做出問答小遊戲!
然後今天原諒我可能沒辦法像前幾天那樣講得很細Q_Q
之前我們只有把 Next.js 的環境佈好,
但今天還會用到 Bootstrap 跟 Reactstrap,
因此我們也需要在專案目錄下進行安裝,
先移到 nextjs-blog
資料夾底下,
執行以下指令:
npm install bootstrap@next
npm install reactstrap
再來就是在 pages
資料夾下新增頁面 game.jsx
,
在 game.jsx
最前面記得 import,
像這樣:
import React, { useState, useEffect } from "react";
import { Container, Row, Col, Button } from "reactstrap"; //這邊放你會用到的 component
import 'bootstrap/dist/css/bootstrap.css';
雖然最後的 code 看起來已經用 React 的方式寫好了,
不過我個人習慣都會是先寫死,
先把架構弄出來,
之後再把它改成 React 語法就不會是太大的問題,
像這樣:
<Container className="w-50 my-4">
<div className="d-flex flex-column align-items-center">
<h3>本屆東京奧運台灣的表現相當亮眼,締造隊史最佳參賽成績。最終奪牌數以下何者正確?</h3>
<img width="300rem" src={`img/01.jpg`}/>
<Row md={2} className="mt-4">
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
1金、2銀、3銅
</Button>
</Col>
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
2金、4銀、6銅
</Button>
</Col>
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
2金、4銀、8銅
</Button>
</Col>
<Col className="my-2">
<Button size="sm" color="info" className="w-50 px-2 py-3">
3金、3銀、6銅
</Button>
</Col>
</Row>
</div>
</Container>
再來因為我想要分成三個畫面:開始遊戲前、遊戲中、遊戲結束,
所以我宣告了一個 status 的狀態,分別為:initial, start, end
(PS. 遊戲中才是遊戲的核心,所以之前的寫死資料我只試遊戲中的部份而已)
... (略)
const [status, setStatus] = useState('initial');
{status === 'initial' && (
... (這邊放開始遊戲前的歡迎畫面)
)}
{status === 'start' && (
... (這邊放開始遊戲的畫面)
)}
{status === 'end' && (
... (這邊放遊戲結束的畫面)
)}
再來我新增了data
的資料夾,
裡面放 question.json
,
內容就是題庫跟選項,
像這樣:
[
{
"question": "本屆東京奧運台灣的表現相當亮眼,締造隊史最佳參賽成績。最終奪牌數以下何者正確?",
"img": "01.jpg",
"options": ["1金、2銀、3銅","2金、4銀、6銅","2金、4銀、8銅","3金、3銀、6銅"],
"answer": 2
},
{
"question": "以下運動項目與本屆奧運台灣出賽選手的配對,何者「有誤」?",
"img": "02.png",
"options": ["柔道─楊勇緯","舉重─郭婞淳","射箭─湯智鈞","桌球─戴資穎"],
"answer": 4
},
... (略)
]
這樣的架構大家應該不陌生,裡面就是物件的陣列,
每個物件分別有 question, img, options, answer 這四個屬性,
分別為題目、圖片、選項、正確答案。
而在前面 import 的地方記得將 json 檔引入,
import questions from '../data/question.json';
既然前面都學過 map 了,我們當然不會想要 hard code 把每一題寫死在頁面上,
這邊我先貼這部份的程式,然後再進行說明:
{status === 'start' ? (
<>
{questions.map((q, index_q) => (
<>
{index_q === selectedIndex ? (
<div className="d-flex flex-column align-items-center">
<h3>Q{index_q+1}. {q.question}</h3>
<img className="my-4" width="300rem" src={`img/${q.img}`}/>
<Row md={2}>
{q.options.map((o, index_o) => (
<>
<Col className="my-2">
<Button size="sm" color="info" value={index_o+1} className={`${styles.btn} px-2 py-3`}
onClick={(e) => {
handleResponse(e);
setSelectedIndex((prev) => prev+1);
}}>
{index_o+1}) {o}
</Button>
</Col>
</>
))}
</Row>
</div>
): null}
</>
))}
</>
): null}
{questions.map((q, index_q) => (
questions 就是先前引入的 json 裡面的物件陣列,
而這邊希望是一題一題出現,
因此我在前面有宣告一個索引,用來記錄現在進行到哪一題:
const [selectedIndex, setSelectedIndex] = useState(0);
所以下面才會有 {index_q === selectedIndex ? (
,
例如現在進行到第1題,只要顯示第1題的內容。(索引值為0)
<h3>Q{index_q+1}. {q.question}</h3>
再來這邊是顯示題目內容,
這邊應該還算直覺,
再下來又出現一個 map 了!
原因是因為選項(options)也是一個陣列(每一題有4個選項),
題目都可以用 map 讀出來了,
選項當然也可以呀~
因此這邊才會這樣寫:
{q.options.map((o, index_o) => (
<>
... (略)
</>
))}
以下是選項的部份:
<Col className="my-2">
<Button size="sm" color="info" value={index_o+1} className={`${styles.btn} px-2 py-3`}
onClick={(e) => {
handleResponse(e);
setSelectedIndex((prev) => prev+1);
}}>
{index_o+1}) {o}
</Button>
</Col>
首先看到 onClick
裡面有兩行,setSelectedIndex((prev) => prev+1)
這行就是將目前進行的題目索引值+1,
再來是 handleResponse(e)
這個函數主要就是進行分數的計算,
這個函數我是這麼寫的:
const handleResponse = (event) => {
if ( parseInt(event.target.value) === questions[selectedIndex].answer ){
alert("恭喜答對!");
setTotalpoints((prev) => prev+1);
} else {
alert("可惜!請再接再勵!");
}
}
當 event.target.value
(點擊目標的 value) 等於 questions[selectedIndex].answer
(現在進行題目的正確答案),
分數就要+1 setTotalpoints((prev) => prev+1)
。
這邊應該是這個遊戲最關鍵的地方,
當你前面寫完,發現每一題都很順利的進行,
可是結束最後一題後畫面變成空白了,
這是因為 questions 裡面的陣列已經全部結束了,
它沒有內容可以顯示只能顯示空白給你看啊XD
但是遊戲結束並不是點擊哪個按鈕之類就是結束遊戲,
因此不適合使用 useState,
所以這邊我們要用到 useEffect 了!
useEffect(() => {
if ( selectedIndex === questions.length ){
setStatus('end');
}
},[selectedIndex]);
偵測題目索引值的變化,
一旦 selectedIndex 等於 questions.length(題庫陣列長度) 表示遊戲結束了,
因此要把狀態切換成 end setStatus('end')
,
一旦狀態為 end,
就要顯示遊戲結束以及顯示總得分,
像這樣:
{status === 'end' && (
<div className="d-flex flex-column align-items-center">
<h2>遊戲結束!</h2>
<img className="my-2" width="500rem" src="img/end.jpg" />
<h3>
共獲得 <span className="h1 text-danger">{totalpoints}</span> 分
</h3>
<Button color="secondary" onClick={resetGame}>
重新遊戲
</Button>
</div>
)}
前面的地方都有用到 <img>
因此你可能有發現 src 的設定不是網址,
而是相對位置的檔案位址 src="img/end.jpg"
,
介紹一下 Next.js 的檔案架構:
圖片供外部存取的檔案都會放在 public 資料夾底下,
因此你可以直接使用 檔名.副檔名
就可以指到該檔案的位址,
至於前面多了 img/
是因為我把圖片放在 img 的資料夾底下,
像這樣:
我們之前在 Codesandbox import CSS 是這樣寫的:
import "./styles.css";
可是當你在 Next.js 引入 CSS 這樣寫卻會出錯:
import styles from '../styles/styles.css';
原因是 XXX.css
在 Next.js 被視為是 Global CSS,
詳細可看官方文件說明→ Global CSS Must Be in Your Custom <App>
如果你真的要自己寫 CSS,
只能將檔案命名為 XXX.module.css
了,
像這樣:
import styles from '../styles/styles.module.css';
而使用 CSS 的語法是這樣:
... className={`${styles.btn} px-2 py-3`} ...
如果 className 只有要放一個單獨自訂的 class,語法這樣寫就好:className={styles.btn}
上面這個寫法是因為還有跟其他 Bootstrap 的 class 擺在一起才要這樣寫。(ES6 的寫法)
[10/4-補充] 因為鐵人賽結束之後我應該不太會再打開這個服務了,
所以這邊還是補一下 CodeSandbox 的連結,
如果想要自己玩玩看的,
不想先被我破梗的可以右轉至此→ Day29 - React 奧運小學堂
------ 我是貼心的防雷設置 ------
------ 我是貼心的防雷設置 ------
------ 我是貼心的防雷設置 ------
然後下面是我試玩的畫面XD
(不過因為題目答案都是我自己用的,所以當然是全部答對XD)
這個系列文可以用這個小遊戲當成結尾真是太好了!!!!!!!
中間一度以為自己寫不出來,
算是及格過關了XD
接著就開心迎接明天的完結篇吧XD