+15

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

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

Google Sheet lưu trữ thông tin đăng kí ở đây: https://docs.google.com/spreadsheets

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. ❤️


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí