Medium: Building a Pick-Me Game with Next.js and Google Sheets API
Để ứng dụng được việc thu thập và xử lý thông tin với Google Sheet API
, trong bài viết này mình giới thiệu cách triển khai mini game thu thập thông tin người chơi lưu vào google sheet và random chọn người chiến thắng ngẫu nhiên từ danh sách dữ liệu trong google sheet.
Yêu cầu
- Next.js
- Google Sheets API
Khởi tạo dự án
Tạo mới ứng dụng NextJS bằng cách chạy các lệnh sau:
npx create-next-app pick-me-game
cd pick-me-game
npm run dev
Sau khi đã có dự án NextJS cơ bản thì mình sang bước tiếp theo.
Xây dựng giao diện game
Ở dự án này chúng ta cần 2 màn hình chính, 1 là trang cho phép người tham gia nhập thông tin fill.js
và một trang cho host random game và pick winner random.js
.
Trong bài viết này mình chỉ muốn tập trung vào cách áp dụng Google Sheets API, nên mình sẽ không giải thích nhiều và các logic khác. Thay vào đó, mình sẽ chia sẻ repository của dự án demo này trên GitHub. Mọi người có thể đọc thêm nếu có thời gian ^^
// pages/fill.js
export default function Fill() { return ( <div> <div className="w-full max-w-xs m-auto mt-4"> <form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> <div className="mb-4"> <input name="name" required className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" type="text" placeholder="Name" /> </div> <div className="mb-6"> <input name="phone" required className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Phone number" /> </div> <div className="flex items-center justify-center"> <button className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" type="submit"> Join random game </button> </div> </form> </div> </div> )
}
// pages/random.js
export default function Random() { //... return ( <div className='btn-start-game'> <button className="btn-game-start border-purple-800 rounded p-3 px-8 bg-white text-purple-600 hover:text-purple-800 border-4 hover:border-purple-800 hover:bg-white font-mono absolute shadow-2xl" onClick={findWinner}> START </button> </div> )
}
Xử lý kết nối/lưu trữ với Google Sheet API
Tạo tài khoản Google API
Truy cập https://console.cloud.google.com/
và tạo tài khoản nếu bạn chưa có. Sau đó chọn Credentials
và Create credentials
> Create service account
để tạo Service account.
Mục tiêu cuối là chúng ta cần 2 keys sau từ Service account GOOGLE_CLIENT_EMAIL
và GOOGLE_PRIVATE_KEY
.
Lưu ý: Tạo service account với quyền của owner.
Thêm 1 key nữa chúng ta cần quan tâm là ID của Google Sheets is GOOGLE_SHEET_ID
, bạn có thể dễ tìm được nó trong link truy cập của cái google sheet đó.
Ví dụ, với link https://docs.google.com/spreadsheets/d/1-6iugU-V9UrO7EDkVt-5x21LN5HeYAzHWgSku9Yy3TA
Thì key ID ở đây là 1-6iugU-V9UrO7EDkVt-5x21LN5HeYAzHWgSku9Yy3TA
. và đừng quên chia sẻ quyền edit cái sheet đó cho email GOOGLE_CLIENT_EMAIL
trong service account đã tạo trước đó.
Xây dựng API để lấy Google Sheet data
Chúng ta cần 3 APIs cơ bản là GET, CREATE, UPDATE.
GET
// pages/api/get
import { NextApiRequest, NextApiResponse } from 'next'
import { google } from 'googleapis' export default async function handler( req: NextApiRequest, res: NextApiResponse
) { try { // prepare auth const auth = new google.auth.GoogleAuth({ credentials: { client_email: process.env.GOOGLE_CLIENT_EMAIL, private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n') }, scopes: [ 'https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/spreadsheets' ] }) const sheets = google.sheets({ auth, version: 'v4' }) const response = await sheets.spreadsheets.values.get({ spreadsheetId: process.env.GOOGLE_SHEET_ID, range: 'A:C', }) return res.status(200).json({ data: response.data }) } catch (e) { console.error(e) return res.status(500).send({ message: 'Something went wrong' }) }
}
CREATE
// pages/api/submit
import { NextApiRequest, NextApiResponse } from 'next'
import { google } from 'googleapis' type SheetForm = { name: string phone: string status: number
} export default async function handler( req: NextApiRequest, res: NextApiResponse
) { if (req.method !== 'POST') { return res.status(405).send({ message: 'Only POST request are allowed' }) } const body = req.body as SheetForm try { // prepare auth const auth = new google.auth.GoogleAuth({ credentials: { client_email: process.env.GOOGLE_CLIENT_EMAIL, private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n') }, scopes: [ 'https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/spreadsheets' ] }) const sheets = google.sheets({ auth, version: 'v4' }) const response = await sheets.spreadsheets.values.append({ spreadsheetId: process.env.GOOGLE_SHEET_ID, range: 'A1:C1', valueInputOption: 'USER_ENTERED', requestBody: { values: [ [body.name, body.phone, body.status] ] } }) return res.status(200).json({ data: response.data }) } catch (e) { console.error(e) return res.status(500).send({ message: 'Something went wrong' }) }
}
UPDATE
// pages/api/update
import { NextApiRequest, NextApiResponse } from 'next'
import { google } from 'googleapis' type SheetData = [] export default async function handler( req: NextApiRequest, res: NextApiResponse
) { if (req.method !== 'POST') { return res.status(405).send({ message: 'Only POST request are allowed' }) } const body = req.body as SheetData try { // prepare auth const auth = new google.auth.GoogleAuth({ credentials: { client_email: process.env.GOOGLE_CLIENT_EMAIL, private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n') }, scopes: [ 'https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/spreadsheets' ] }) const sheets = google.sheets({ auth, version: 'v4' }) const response = await sheets.spreadsheets.values.update({ spreadsheetId: process.env.GOOGLE_SHEET_ID, range: 'A:C', valueInputOption: 'USER_ENTERED', requestBody: { values: body } }) return res.status(200).json({ data: response.data }) } catch (e) { console.error(e) return res.status(500).send({ message: 'Something went wrong' }) }
}
Xử lý logic
Xử lý thông tin người tham gia có hợp lệ không dựa trên sđt lấy từ Google Sheets via API.
// fill.js
import { useEffect, useState } from 'react'
// ...
const [data, setData] = useState([])
const [isLoad, setIsLoad] = useState(false) const fetchData = async () => { const req = await fetch('/api/get') const res = await req.json() if (res.data && res.data.values) { setData(res.data.values) } setIsLoad(true)
} useEffect(() => { fetchData()
}, []) const handleClick = async (e) => { e.preventDefault() const name = document.querySelector('#name').value const phone = document.querySelector('#phone').value const status = 1 let checkPhone = 0 if (data.length > 0) { for (let i = 0; i <= data.length; i++) { // break condition if (data[i] && data[i][1] == phone) { console.log(data[i][1]) setErrorText('Joined phone number!') setError(true) checkPhone = 1 break; } } } if (checkPhone == 1) { return false } //... const form = { name, phone, status } const response = await fetch('/api/submit', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(form) }) const content = await response.json() console.log(content)
}
// ...
Chọn người chiến thắng ngẫu nhiên
// ramdom.js
import { useEffect, useState, useRef } from 'react'
// ...
const [gameState, setGameState] = useState(false)
const [data, setData] = useState([])
const [showLabel, setShowLabel] = useState(false)
const [index, setIndex] = useState(null) const handleClick = (state) => { setGameState(state)
} const fetchData = async () => { const req = await fetch('/api/get') const res = await req.json() if (res.data && res.data.values) { setData(res.data.values) }
} useEffect(() => { fetchData()
}, []) const findWinner = async () => { var winnerIdx = Math.floor(Math.random() * data.length) var newData = [] if (data[winnerIdx][2] == 0) { findWinner() } setLoading(true) setTimeout(() => { setIndex(winnerIdx) setShowLabel(true) setLoading(false) }, 5000) // Update data data.forEach((item, i) => { newData[i] = item if (winnerIdx == i) { newData[i] = [item[0], item[1], 0] } }) const response = await fetch('/api/update', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(newData) }) const content = await response.json() console.log(content) fetchData()
}
// ...
Demo game
Trang đăng kí cho người tham gia join the game
Trang cho host game tại đây: Start game
Chọn người chiến thắng với button "START"
Bạn có xem demo chi tiết hơn ở đây: pickme.bunhere.com
End
Author: bunhere.com
I am always looking for feedback on my writing, so please let me know what you think. ❤️