- vừa được xem lúc

Xây dựng PickMe game đơn giản với NextJS và Google Sheet API

0 0 7

Người đăng: Bun

Theo Viblo Asia

Medium: Building a Pick-Me Game with Next.js and Google Sheets API

Google Sheet 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 CredentialsCreate credentials > Create service account để tạo Service account.

ggcloud

Mục tiêu cuối là chúng ta cần 2 keys sau từ Service account GOOGLE_CLIENT_EMAILGOOGLE_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

fill

Trang cho host game tại đây: Start game

main

Chọn người chiến thắng với button "START"

winner

Bạn có xem demo chi tiết hơn ở đây: pickme.bunhere.com

pickme

End

Author: bunhere.com

I am always looking for feedback on my writing, so please let me know what you think. ❤️

Bình luận

Bài viết tương tự

- vừa được xem lúc

Thủ thuật nhỏ để căn chỉnh image với object-fit

Chào các bạn,. Có lẽ trong hành trình code của các bạn thì không ít lần gặp vấn đề méo ảnh do fix cứng cả width, height của ảnh nhỉ? Hoặc kể cả khi bạn set value cho 1 thuộc tính weigth hoặc height còn thuộc tính còn lại để auto thì nhiều lúc ảnh cũng không được hiển thị toàn vẹn cho lắm.

0 0 34

- vừa được xem lúc

Tìm hiểu về CSS framework - Bulma

Mở đầu:. Mấy bữa nay đang lướt web thấy có giới thiệu framework bulma này, được mọi người giới thiệu gọn nhẹ và dễ sử dụng, nên mình mới tìm hiểu thử và hôm nay xin viết 1 bài viết giới thiệu sơ qua với các bạn.

0 0 25

- vừa được xem lúc

Một số mẹo vặt "hay ho" của ES6 có thể bạn chưa biết - Phần 4

Xin chào, ở 3 bài trước của series "Một số mẹo vặt "hay ho" của ES6", mình đã chia sẻ 1 số tips/tricks nhỏ với ES6, hy vọng ít nhiều nó sẽ có ích với các bạn khi áp dụng vào thực tế. Hôm nay, xin mời các bạn theo dõi phần 4 của series này.

0 0 30

- vừa được xem lúc

Tìm hiểu về Jest Mocks Test phía frontend

Giới thiệu. Chắc hẳn không ai phủ nhận rằng UnitTest là 1 phần quan trọng trong giai đoạn phát triển phần mềm, đảm bảo cho code được coverage tránh các bug không mong muốn.

0 0 24

- vừa được xem lúc

Convert từ SVG sang Icon Font như thế nào?

Chào các bạn. Như câu hỏi trên title của bài viết, hôm nay mình sẽ hướng dẫn các bạn cách convert 1 file svg 1 cách khá đơn giản và vô cùng tiện lợi cho các bạn. https://icomoon.io/app/#/select.

0 0 40

- vừa được xem lúc

Một vài thủ thuật làm việc với các dạng layout - Phần 4

. Chào mọi người, cũng đã lâu rồi mình không thấy nhau. Để tiếp tục với series's về các dạng layout hôm nay mình sẽ chia sẻ thêm một trick thú vị nữa về step layout.

0 0 31