Implemented simple full-stack logic (unfinished)

This commit is contained in:
aronmal 2023-04-12 22:33:29 +02:00
parent a32b40395e
commit 2862f94f1c
Signed by: aronmal
GPG key ID: 816B7707426FC612
19 changed files with 481 additions and 189 deletions

View file

@ -14,7 +14,7 @@ function EventBar({
{ icon: "burger-menu", text: "Menu" },
{ icon: "radar", text: "Radar scan", mode: 0, amount: 1 },
{ icon: "torpedo", text: "Fire torpedo", mode: 1, amount: 1 },
{ icon: "scope", text: "Fire missile", mode: 2 },
{ icon: "scope", text: "Fire missile", mode: 3 },
{ icon: "gear", text: "Settings" },
]
return (

View file

@ -5,7 +5,7 @@ import { Fragment, useContext, useEffect, useState } from "react"
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
const [gameProps, setGameProps] = useContext(gameContext)
const [enemy, setEnemy] = useState(false)
const { enemy } = gameProps
const [dots, setDots] = useState(1)
useEffect(() => {
@ -28,18 +28,13 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
<div className="flex items-center justify-around">
<Player
src="player_blue.png"
text="Spieler 1 (Du)"
text={gameProps.player?.username ?? "Spieler 1 (Du)"}
primary={true}
edit={true}
/>
<p
className="font-farro m-4 text-6xl font-semibold"
onClick={() => setEnemy((e) => !e)}
>
VS
</p>
<p className="font-farro m-4 text-6xl font-semibold">VS</p>
{enemy ? (
<Player src="player_red.png" text="Spieler 2" />
<Player src="player_red.png" text={enemy.username ?? "Spieler 2"} />
) : (
<p className="font-farro w-96 text-center text-5xl font-medium">
Warte auf Spieler 2 {Array.from(Array(dots), () => ".").join("")}

View file

@ -11,7 +11,7 @@ export interface IdToken {
type: TokenType
}
const tokenLifetime = {
export const tokenLifetime = {
REFRESH: 172800,
ACCESS: 15,
}

View file

@ -1,4 +1,3 @@
import { rejectionErrors } from "../errors"
import sendError, { API } from "./sendError"
async function getPinFromBody<T>(context: API<T>, next: (pin: string) => void) {
@ -9,7 +8,13 @@ async function getPinFromBody<T>(context: API<T>, next: (pin: string) => void) {
!("pin" in body) ||
typeof body.pin !== "string"
)
return sendError(context, rejectionErrors.noUsername)
return sendError(context, {
rejected: true,
message: "No pin in request body!",
statusCode: 401,
solved: true,
type: "warn",
})
const { pin } = body
return next(pin)

View file

@ -3,6 +3,7 @@ import { rejectionErrorFns, rejectionErrors } from "../errors"
import type { IdToken } from "./createTokenDB"
import sendError, { API } from "./sendError"
import type { Token } from "@prisma/client"
import { deleteCookie } from "cookies-next"
async function getTokenDB<T>(
context: API<T>,
@ -17,7 +18,10 @@ async function getTokenDB<T>(
id,
},
})
if (!tokenDB) return sendError(context, rejectionErrorFns.tokenNotFound(type))
if (!tokenDB) {
deleteCookie("token", { ...context, path: "/api" })
return sendError(context, rejectionErrorFns.tokenNotFound(type))
}
if (tokenDB.used && !ignoreChecks)
return sendError(context, rejectionErrors.tokenUsed)

View file

@ -18,6 +18,7 @@ async function getTokenFromBody<T>(
const value = body.token
return next({ value, type })
}
console.log(body)
return sendError(context, rejectionErrorFns.noToken(type))
}

View file

@ -1,32 +1,24 @@
import createPlayerDB from "./createPlayerDB"
import createTokenDB, { RawToken } from "./createTokenDB"
import type { API } from "./sendError"
import { setCookie } from "cookies-next"
import { Player, Token } from "@prisma/client"
async function getTokenFromCookie<T>(
context: API<T>,
next: (refreshToken: RawToken) => void
next: (
refreshToken: RawToken,
newPlayer?: { player: Player; newToken: RawToken; newTokenDB: Token }
) => void
) {
const type = "REFRESH"
const { req, res } = context
const value = req.cookies.token
const value = context.req.cookies.token
// Checking for cookie presens, because it is necessary
if (!value) {
return createPlayerDB((player) =>
createTokenDB(player, type, (newToken) => {
// Set login cookie
setCookie("token", newToken.value, {
req,
res,
maxAge: 172800,
httpOnly: true,
sameSite: true,
secure: true,
path: "/api",
})
return next(newToken)
})
createTokenDB(player, type, (newToken, newTokenDB) =>
next(newToken, { player, newToken, newTokenDB })
)
)
}
return next({ value, type })

View file

@ -1,15 +1,39 @@
import logging, { Logging } from "../logging"
import { RawToken, tokenLifetime } from "./createTokenDB"
import type { API } from "./sendError"
import { deleteCookie, setCookie } from "cookies-next"
export interface Result<T> {
message: string
statusCode?: number
body?: T
type?: Logging[]
cookie?: string
}
export default function sendResponse<T>(context: API<T>, result: Result<T>) {
const { req, res } = context
if (typeof result.cookie === "string") {
if (result.cookie) {
console.log(1)
setCookie("token", result.cookie, {
req,
res,
maxAge: tokenLifetime.REFRESH,
httpOnly: true,
sameSite: true,
secure: true,
path: "/api",
})
} else {
console.log(2)
deleteCookie("token", {
req,
res,
path: "/api",
})
}
}
res.status(result.statusCode ?? 200)
result.body ? res.json(result.body) : res.end()
logging(result.message, result.type ?? ["debug"], req)

View file

@ -69,6 +69,12 @@ export const rejectionErrors: rejectionErrors = {
solved: true,
type: "warn",
},
gameNotFound: {
rejected: true,
message: "Game not found!",
statusCode: 403,
solved: true,
},
}
export const rejectionErrorFns: rejectionErrorFns = {

View file

@ -1,18 +1,42 @@
import status from "http-status"
import { toast } from "react-toastify"
import { ZodError, z } from "zod"
const tokenSchema = z.object({
token: z.string(),
})
export function successfulResponse(res: Response) {
if (status[`${res.status}_CLASS`] === status.classes.SUCCESSFUL)
return res.json()
const resStatus = status[`${res.status}_CLASS`]
if (typeof resStatus !== "string") return
toast(status.classes[resStatus] + ": " + status[res.status], {
position: "top-center",
type: "error",
theme: "colored",
})
return Promise.reject()
}
async function getAccessToken() {
const response = await fetch("/api/user/auth", {
return fetch("/api/user/auth", {
method: "GET",
})
const res = await response.json()
.then(successfulResponse)
.then((response) => {
try {
const token = tokenSchema.parse(response)
if (
typeof res === "object" &&
res &&
"token" in res &&
typeof res.token === "string"
)
return res.token
throw new Error("Access token not found")
return token
} catch (err: any) {
const error = err as ZodError
toast(JSON.stringify(error))
return Promise.reject()
}
})
}
export default getAccessToken

View file

@ -25,11 +25,14 @@
"cookies-next": "^2.1.1",
"eslint": "8.31.0",
"eslint-config-next": "13.1.1",
"http-status": "^1.6.2",
"jsonwebtoken": "^9.0.0",
"next": "13.1.1",
"prisma": "^4.12.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-otp-input": "^3.0.0",
"react-toastify": "^9.1.2",
"socket.io": "^4.6.1",
"socket.io-client": "^4.6.1",
"typescript": "4.9.4",

View file

@ -4,11 +4,18 @@ import "../styles/grid2.scss"
import "../styles/grid.scss"
import type { AppProps } from "next/app"
import { Dispatch, SetStateAction, createContext, useState } from "react"
import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
interface gameContext {
pin?: string
game?: {
id: ""
id: string
}
player?: {
id: string
username?: string
isOwner?: boolean
}
enemy?: {
id: string
@ -25,6 +32,7 @@ export default function App({ Component, pageProps }: AppProps) {
return (
<gameContext.Provider value={gameProps}>
<Component {...pageProps} />
<ToastContainer />
</gameContext.Provider>
)
}

View file

@ -2,18 +2,33 @@ import sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse"
import getPlayer from "@lib/backend/components/getPlayer"
import prisma from "@lib/prisma"
import { Game, Gamepin, Player_Game } from "@prisma/client"
import type { NextApiRequest, NextApiResponse } from "next"
import { z } from "zod"
const returnSchema = z.object({
export const createSchema = z.object({
game: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
running: z.boolean(),
}),
pin: z.string().optional(),
player: z.object({
id: z.string(),
username: z.string().optional(),
isOwner: z.boolean().optional(),
}),
enemy: z
.object({
id: z.string(),
username: z.string().optional(),
isOwner: z.boolean().optional(),
})
.optional(),
})
type Data = z.infer<typeof returnSchema>
type Data = z.infer<typeof createSchema>
export default async function create(
req: NextApiRequest,
@ -26,16 +41,20 @@ export default async function create(
const pin = Math.floor(Math.random() * 10000)
.toString()
.padStart(4, "0")
const game = await prisma.game.create({
data: {
pin: {
create: {
pin,
},
},
let created = false
let game:
| (Game & {
pin: Gamepin | null
players: Player_Game[]
})
| null
game = await prisma.game.findFirst({
where: {
running: true,
players: {
create: {
isOwner: true,
some: {
playerId: player.id,
},
},
@ -45,10 +64,41 @@ export default async function create(
players: true,
},
})
if (!game) {
created = true
game = await prisma.game.create({
data: {
pin: {
create: {
pin,
},
},
players: {
create: {
isOwner: true,
playerId: player.id,
},
},
},
include: {
pin: true,
players: true,
},
})
}
return sendResponse(context, {
message: `Player: ${player.id} created game: ${game.id}`,
body: { game },
statusCode: created ? 201 : 200,
body: {
game,
pin: game.pin?.pin,
player: {
id: player.id,
username: player.username ?? undefined,
isOwner: true,
},
},
type: ["debug", "infoCyan"],
})
}).catch((err) => sendError(context, err))

View file

@ -2,6 +2,7 @@ import sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse"
import getPinFromBody from "@lib/backend/components/getPinFromBody"
import getPlayer from "@lib/backend/components/getPlayer"
import { rejectionErrors } from "@lib/backend/errors"
import prisma from "@lib/prisma"
import type { Game } from "@prisma/client"
import type { NextApiRequest, NextApiResponse } from "next"
@ -18,30 +19,61 @@ export default async function join(
return getPinFromBody(context, (pin) =>
getPlayer(context, async (player) => {
const { game } = await prisma.gamepin.update({
where: {
pin,
},
data: {
game: {
update: {
players: {
create: {
isOwner: false,
playerId: player.id,
try {
const pinDB = await prisma.gamepin.update({
where: {
pin,
},
data: {
game: {
update: {
players: {
create: {
isOwner: false,
playerId: player.id,
},
},
},
},
},
},
include: { game: true },
})
include: {
game: {
include: {
players: {
include: {
player: true,
},
},
},
},
},
})
sendResponse(context, {
message: `Player: ${player.id} joined game: ${game.id}`,
body: { game },
type: ["debug", "infoCyan"],
})
const enemy = pinDB.game.players.find(
(enemy) => enemy.player.id !== player.id
)
return sendResponse(context, {
message: `Player: ${player.id} joined game: ${pinDB.game.id}`,
body: {
game: pinDB.game,
pin: pinDB.pin,
player: {
id: player.id,
username: player.username ?? undefined,
isOwner: true,
},
enemy: {
id: enemy?.player.id,
username: enemy?.player.username ?? undefined,
isOwner: false,
},
},
type: ["debug", "infoCyan"],
})
} catch (err: any) {
console.log("HERE".red, err.code, err.meta, err.message)
return sendError(context, rejectionErrors.gameNotFound)
}
})
).catch((err) => sendError(context, err))
}

View file

@ -5,6 +5,7 @@ import getTokenDB from "@backend/components/getTokenDB"
import getTokenFromCookie from "@backend/components/getTokenFromCookie"
import sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse"
import { Player, Token } from "@prisma/client"
import type { NextApiRequest, NextApiResponse } from "next"
interface Data {
@ -18,19 +19,24 @@ export default async function auth(
const context = { req, res }
const type = "ACCESS"
return getTokenFromCookie(context, (refreshToken) =>
checkTokenIsValid(context, refreshToken, (token) =>
getTokenDB(context, token, (tokenDB) =>
getPlayerByIdDB(context, tokenDB, (player) =>
createTokenDB(player, type, (newToken, newTokenDB) =>
sendResponse(context, {
message: `Access-Token generated: ${newTokenDB.id} with Refreshtoken-Token: ${tokenDB.id}`,
body: { token: newToken.value },
type: ["debug", "infoCyan"],
})
)
return getTokenFromCookie(context, (refreshToken, newPlayer) => {
const next = (player: Player, tokenDB: Token, cookie?: string) =>
createTokenDB(player, type, (newToken, newTokenDB) =>
sendResponse(context, {
message: `Access-Token generated: ${newTokenDB.id} with Refreshtoken-Token: ${tokenDB.id}`,
body: { token: newToken.value },
type: ["debug", "infoCyan"],
cookie,
})
)
if (!newPlayer) {
return checkTokenIsValid(context, refreshToken, (token) =>
getTokenDB(context, token, (tokenDB) =>
getPlayerByIdDB(context, tokenDB, (player) => next(player, tokenDB))
)
)
)
).catch((err) => sendError(context, err))
}
const { player, newToken, newTokenDB } = newPlayer
return next(player, newTokenDB, newToken.value)
}).catch((err) => sendError(context, err))
}

View file

@ -26,17 +26,6 @@ export default async function login(
getPlayerByNameDB(context, username, (player) => {
checkPasswordIsValid(context, player, password, () => {
createTokenDB(player, "REFRESH", (newToken, newTokenDB) => {
// Set login cookie
setCookie("token", newToken.value, {
req,
res,
maxAge: 172800,
httpOnly: true,
sameSite: true,
secure: true,
path: "/api",
})
sendResponse(context, {
message:
"User " +
@ -45,6 +34,7 @@ export default async function login(
newTokenDB.id,
body: { loggedIn: true },
type: ["debug", "infoCyan"],
cookie: newToken.value,
})
})
})

View file

@ -3,7 +3,6 @@ import getTokenFromCookie from "@backend/components/getTokenFromCookie"
import sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse"
import decodeToken from "@lib/backend/components/decodeToken"
import { deleteCookie } from "cookies-next"
import type { NextApiRequest, NextApiResponse } from "next"
interface Data {
@ -12,7 +11,7 @@ interface Data {
export default async function logout(
req: NextApiRequest,
res: NextApiResponse<any>
res: NextApiResponse<Data>
) {
const context = { req, res }
@ -22,13 +21,11 @@ export default async function logout(
context,
token,
(tokenDB) => {
// Set login cookie
deleteCookie("token", { req, res })
return sendResponse(context, {
message: "User of Token " + tokenDB.id + " logged out.",
body: { loggedOut: true },
type: ["debug", "infoCyan"],
cookie: "",
})
},
true

View file

@ -1,104 +1,222 @@
import BurgerMenu from "../../components/BurgerMenu"
import Logo from "../../components/Logo"
import OptionButton from "../../components/OptionButton"
import { gameContext } from "../_app"
import { faEye, faLeftLong } from "@fortawesome/pro-regular-svg-icons"
import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons"
import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import getAccessToken from "@lib/frontend/getAccessToken"
import getAccessToken, {
successfulResponse,
} from "@lib/frontend/getAccessToken"
import { GetServerSideProps } from "next"
import { useRouter } from "next/router"
import { useState } from "react"
import { useCallback, useContext, useEffect, useState } from "react"
import OtpInput from "react-otp-input"
import { toast } from "react-toastify"
import { z } from "zod"
interface Props {
start: boolean
q: string | string[] | undefined
}
function isInputOnlyNumbers(input: string) {
return /^\d+$/.test(input)
}
export default function Home({ start }: Props) {
export default function Home({ q }: Props) {
const [otp, setOtp] = useState("")
const [gameProps, setGameProps] = useContext(gameContext)
const router = useRouter()
const gameFetch = useCallback(
async (pin?: string) => {
const createSchema = z.object({
game: z.object({
id: z.string(),
}),
pin: z.string().optional(),
player: z.object({
id: z.string(),
username: z.string().optional(),
isOwner: z.boolean().optional(),
}),
enemy: z
.object({
id: z.string(),
username: z.string().optional(),
isOwner: z.boolean().optional(),
})
.optional(),
})
const gamePromise = getAccessToken().then((token) => {
console.log(otp, { ...token, pin })
return fetch("/api/game/" + (!pin ? "create" : "join"), {
method: "POST",
body: JSON.stringify({ ...token, pin: pin }),
})
.then(successfulResponse)
.then((game) => createSchema.parse(game))
})
const res = await toast.promise(gamePromise, {
pending: {
render: "Raum wird " + (!pin ? "erstellt" : "angefragt"),
},
success: {
render: "Raum " + (!pin ? "erstellt" : "angefragt") + " 👌",
type: "info",
theme: "colored",
},
error: {
render: "Es ist ein Fehler aufgetreten 🤯",
type: "error",
theme: "colored",
},
})
setGameProps(res)
await toast.promise(router.push("/dev/lobby"), {
pending: {
render: "Raum wird beigetreten",
},
success: {
render: "Raum begetreten 👌",
type: "info",
},
error: {
render: "Es ist ein Fehler aufgetreten 🤯",
type: "error",
},
})
},
[router, setGameProps]
)
useEffect(() => {
if (otp.length !== 4) return
if (!isInputOnlyNumbers(otp)) {
toast("Der Code darf nur Zahlen beinhalten!", {
type: "warning",
theme: "dark",
})
return
}
gameFetch(otp)
}, [otp])
return (
<div className="h-full bg-theme">
<div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
<Logo />
<BurgerMenu />
{!start ? (
<>
<div className="flex h-36 w-64 items-center justify-center rounded-xl border-4 border-black bg-[#2227] sm:h-48 sm:w-96 md:h-72 md:w-[32rem] md:border-[6px] xl:h-[26rem] xl:w-[48rem]">
<FontAwesomeIcon
className="text-6xl sm:text-7xl md:text-8xl"
icon={faCirclePlay}
/>
</div>
<button
className="font-farro rounded-lg border-b-4 border-orange-400 bg-warn px-12 pb-4 pt-5 text-2xl font-bold duration-100 active:border-b-0 active:border-t-4 sm:rounded-xl sm:border-b-[6px] sm:px-14 sm:pb-5 sm:pt-6 sm:text-3xl sm:active:border-t-[6px] md:rounded-2xl md:border-b-8 md:px-20 md:pb-6 md:pt-7 md:text-4xl md:active:border-t-8 xl:px-24 xl:pb-8 xl:pt-10 xl:text-5xl"
onClick={() =>
setTimeout(() => {
// Navigate to the same page with the `start` query parameter set to `true`
router.push({
pathname: router.pathname,
query: { start: true },
})
}, 200)
}
>
START
</button>
</>
) : (
<div className="flex flex-col items-center rounded-xl border-4 border-black bg-grayish px-4 py-6 shadow-lg sm:mx-8 sm:p-12 md:w-full">
<button
className="-mt-2 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl"
onClick={() =>
setTimeout(() => {
// Navigate to the same page with the `start` query parameter set to `false`
router.push({
pathname: router.pathname,
})
}, 200)
}
>
<FontAwesomeIcon icon={faLeftLong} />
</button>
<div className="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton
action={async () => {
const token = await getAccessToken()
const game = await fetch("/api/game/create", {
method: "POST",
body: JSON.stringify({ token }),
})
const gameSchema = z.object({
game: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
running: z.boolean(),
}),
})
const check = gameSchema.safeParse(game)
if (check.success) {
const gameData = check.data
console.log(gameData)
} else {
console.error(check.error)
}
// const warst = result.game
router.push("/dev/lobby")
}}
icon={faPlus}
>
Raum erstellen
</OptionButton>
<OptionButton icon={faUserPlus}>Raum beitreten</OptionButton>
<OptionButton icon={faEye}>Zuschauen</OptionButton>
</div>
</div>
)}
{(() => {
switch (q) {
case "join":
return (
<div className="flex flex-col items-center rounded-xl border-4 border-black bg-grayish px-4 py-6 shadow-lg sm:mx-8 sm:p-12 md:w-full">
<button
className="-mt-2 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl"
onClick={() =>
setTimeout(() => {
// Navigate to the same page with the `start` query parameter set to `false`
router.push({
pathname: router.pathname,
query: null,
})
}, 200)
}
>
<FontAwesomeIcon icon={faLeftLong} />
</button>
<div className="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton icon={faPlus}>Raum erstellen</OptionButton>
<OptionButton
action={() => {
router.push({
pathname: router.pathname,
query: { q: "join" },
})
}}
icon={faUserPlus}
>
<OtpInput
containerStyle={{ color: "initial" }}
value={otp}
onChange={setOtp}
numInputs={4}
placeholder="0000"
renderSeparator={<span>-</span>}
renderInput={(props) => <input {...props} />}
/>
</OptionButton>
<OptionButton icon={faEye}>Zuschauen</OptionButton>
</div>
</div>
)
case "start":
return (
<div className="flex flex-col items-center rounded-xl border-4 border-black bg-grayish px-4 py-6 shadow-lg sm:mx-8 sm:p-12 md:w-full">
<button
className="-mt-2 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl"
onClick={() =>
setTimeout(() => {
// Navigate to the same page with the `start` query parameter set to `false`
router.push({
pathname: router.pathname,
query: null,
})
}, 200)
}
>
<FontAwesomeIcon icon={faLeftLong} />
</button>
<div className="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton action={() => gameFetch()} icon={faPlus}>
Raum erstellen
</OptionButton>
<OptionButton
action={() => {
router.push({
pathname: router.pathname,
query: { q: "join" },
})
}}
icon={faUserPlus}
>
Raum beitreten
</OptionButton>
<OptionButton icon={faEye}>Zuschauen</OptionButton>
</div>
</div>
)
default:
return (
<>
<div className="flex h-36 w-64 items-center justify-center rounded-xl border-4 border-black bg-[#2227] sm:h-48 sm:w-96 md:h-72 md:w-[32rem] md:border-[6px] xl:h-[26rem] xl:w-[48rem]">
<FontAwesomeIcon
className="text-6xl sm:text-7xl md:text-8xl"
icon={faCirclePlay}
/>
</div>
<button
className="font-farro rounded-lg border-b-4 border-orange-400 bg-warn px-12 pb-4 pt-5 text-2xl font-bold duration-100 active:border-b-0 active:border-t-4 sm:rounded-xl sm:border-b-[6px] sm:px-14 sm:pb-5 sm:pt-6 sm:text-3xl sm:active:border-t-[6px] md:rounded-2xl md:border-b-8 md:px-20 md:pb-6 md:pt-7 md:text-4xl md:active:border-t-8 xl:px-24 xl:pb-8 xl:pt-10 xl:text-5xl"
onClick={() =>
setTimeout(() => {
// Navigate to the same page with the `start` query parameter set to `true`
router.push({
pathname: router.pathname,
query: { q: "start" },
})
}, 200)
}
>
START
</button>
</>
)
}
})()}
</div>
</div>
)
@ -107,10 +225,7 @@ export default function Home({ start }: Props) {
export const getServerSideProps: GetServerSideProps<Props> = async (
context
) => {
const { start } = context.query
const { q } = context.query
// Convert the `start` query parameter to a boolean
const isStart = start === "true"
return { props: { start: isStart } }
return { props: { q: q ? q : "" } }
}

View file

@ -49,6 +49,9 @@ dependencies:
eslint-config-next:
specifier: 13.1.1
version: 13.1.1(eslint@8.31.0)(typescript@4.9.4)
http-status:
specifier: ^1.6.2
version: 1.6.2
jsonwebtoken:
specifier: ^9.0.0
version: 9.0.0
@ -64,6 +67,12 @@ dependencies:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-otp-input:
specifier: ^3.0.0
version: 3.0.0(react-dom@18.2.0)(react@18.2.0)
react-toastify:
specifier: ^9.1.2
version: 9.1.2(react-dom@18.2.0)(react@18.2.0)
socket.io:
specifier: ^4.6.1
version: 4.6.1
@ -1010,6 +1019,11 @@ packages:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false
/clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
dev: false
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@ -1920,6 +1934,11 @@ packages:
dependencies:
function-bind: 1.1.1
/http-status@1.6.2:
resolution: {integrity: sha512-oUExvfNckrpTpDazph7kNG8sQi5au3BeTo0idaZFXEhTaJKu7GNJCLHI0rYY2wljm548MSTM+Ljj/c6anqu2zQ==}
engines: {node: '>= 0.4.0'}
dev: false
/https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@ -2785,6 +2804,27 @@ packages:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: false
/react-otp-input@3.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-TgTE3PbHhu4VxsAA9JUACzhtxwuZ8OSle9kiwK0Ne7fCv7wOXTtIRWQWPoNlfOZ/sxGsXjMexqwrQdB9yy0qEQ==}
peerDependencies:
react: '>=16.8.6 || ^17.0.0 || ^18.0.0'
react-dom: '>=16.8.6 || ^17.0.0 || ^18.0.0'
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-toastify@9.1.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-PBfzXO5jMGEtdYR5jxrORlNZZe/EuOkwvwKijMatsZZm8IZwLj01YvobeJYNjFcA6uy6CVrx2fzL9GWbhWPTDA==}
peerDependencies:
react: '>=16'
react-dom: '>=16'
dependencies:
clsx: 1.2.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}