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: "burger-menu", text: "Menu" },
{ icon: "radar", text: "Radar scan", mode: 0, amount: 1 }, { icon: "radar", text: "Radar scan", mode: 0, amount: 1 },
{ icon: "torpedo", text: "Fire torpedo", mode: 1, 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" }, { icon: "gear", text: "Settings" },
] ]
return ( return (

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import { rejectionErrors } from "../errors"
import sendError, { API } from "./sendError" import sendError, { API } from "./sendError"
async function getPinFromBody<T>(context: API<T>, next: (pin: string) => void) { 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) || !("pin" in body) ||
typeof body.pin !== "string" 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 const { pin } = body
return next(pin) return next(pin)

View file

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

View file

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

View file

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

View file

@ -1,15 +1,39 @@
import logging, { Logging } from "../logging" import logging, { Logging } from "../logging"
import { RawToken, tokenLifetime } from "./createTokenDB"
import type { API } from "./sendError" import type { API } from "./sendError"
import { deleteCookie, setCookie } from "cookies-next"
export interface Result<T> { export interface Result<T> {
message: string message: string
statusCode?: number statusCode?: number
body?: T body?: T
type?: Logging[] type?: Logging[]
cookie?: string
} }
export default function sendResponse<T>(context: API<T>, result: Result<T>) { export default function sendResponse<T>(context: API<T>, result: Result<T>) {
const { req, res } = context 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) res.status(result.statusCode ?? 200)
result.body ? res.json(result.body) : res.end() result.body ? res.json(result.body) : res.end()
logging(result.message, result.type ?? ["debug"], req) logging(result.message, result.type ?? ["debug"], req)

View file

@ -69,6 +69,12 @@ export const rejectionErrors: rejectionErrors = {
solved: true, solved: true,
type: "warn", type: "warn",
}, },
gameNotFound: {
rejected: true,
message: "Game not found!",
statusCode: 403,
solved: true,
},
} }
export const rejectionErrorFns: rejectionErrorFns = { 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() { async function getAccessToken() {
const response = await fetch("/api/user/auth", { return fetch("/api/user/auth", {
method: "GET", method: "GET",
}) })
const res = await response.json() .then(successfulResponse)
.then((response) => {
try {
const token = tokenSchema.parse(response)
if ( return token
typeof res === "object" && } catch (err: any) {
res && const error = err as ZodError
"token" in res && toast(JSON.stringify(error))
typeof res.token === "string" return Promise.reject()
) }
return res.token })
throw new Error("Access token not found")
} }
export default getAccessToken export default getAccessToken

View file

@ -25,11 +25,14 @@
"cookies-next": "^2.1.1", "cookies-next": "^2.1.1",
"eslint": "8.31.0", "eslint": "8.31.0",
"eslint-config-next": "13.1.1", "eslint-config-next": "13.1.1",
"http-status": "^1.6.2",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"next": "13.1.1", "next": "13.1.1",
"prisma": "^4.12.0", "prisma": "^4.12.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "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": "^4.6.1",
"socket.io-client": "^4.6.1", "socket.io-client": "^4.6.1",
"typescript": "4.9.4", "typescript": "4.9.4",

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import getTokenDB from "@backend/components/getTokenDB"
import getTokenFromCookie from "@backend/components/getTokenFromCookie" import getTokenFromCookie from "@backend/components/getTokenFromCookie"
import sendError from "@backend/components/sendError" import sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse" import sendResponse from "@backend/components/sendResponse"
import { Player, Token } from "@prisma/client"
import type { NextApiRequest, NextApiResponse } from "next" import type { NextApiRequest, NextApiResponse } from "next"
interface Data { interface Data {
@ -18,19 +19,24 @@ export default async function auth(
const context = { req, res } const context = { req, res }
const type = "ACCESS" const type = "ACCESS"
return getTokenFromCookie(context, (refreshToken) => return getTokenFromCookie(context, (refreshToken, newPlayer) => {
checkTokenIsValid(context, refreshToken, (token) => const next = (player: Player, tokenDB: Token, cookie?: string) =>
getTokenDB(context, token, (tokenDB) => createTokenDB(player, type, (newToken, newTokenDB) =>
getPlayerByIdDB(context, tokenDB, (player) => sendResponse(context, {
createTokenDB(player, type, (newToken, newTokenDB) => message: `Access-Token generated: ${newTokenDB.id} with Refreshtoken-Token: ${tokenDB.id}`,
sendResponse(context, { body: { token: newToken.value },
message: `Access-Token generated: ${newTokenDB.id} with Refreshtoken-Token: ${tokenDB.id}`, type: ["debug", "infoCyan"],
body: { token: newToken.value }, cookie,
type: ["debug", "infoCyan"], })
}) )
) 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) => { getPlayerByNameDB(context, username, (player) => {
checkPasswordIsValid(context, player, password, () => { checkPasswordIsValid(context, player, password, () => {
createTokenDB(player, "REFRESH", (newToken, newTokenDB) => { 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, { sendResponse(context, {
message: message:
"User " + "User " +
@ -45,6 +34,7 @@ export default async function login(
newTokenDB.id, newTokenDB.id,
body: { loggedIn: true }, body: { loggedIn: true },
type: ["debug", "infoCyan"], 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 sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse" import sendResponse from "@backend/components/sendResponse"
import decodeToken from "@lib/backend/components/decodeToken" import decodeToken from "@lib/backend/components/decodeToken"
import { deleteCookie } from "cookies-next"
import type { NextApiRequest, NextApiResponse } from "next" import type { NextApiRequest, NextApiResponse } from "next"
interface Data { interface Data {
@ -12,7 +11,7 @@ interface Data {
export default async function logout( export default async function logout(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<any> res: NextApiResponse<Data>
) { ) {
const context = { req, res } const context = { req, res }
@ -22,13 +21,11 @@ export default async function logout(
context, context,
token, token,
(tokenDB) => { (tokenDB) => {
// Set login cookie
deleteCookie("token", { req, res })
return sendResponse(context, { return sendResponse(context, {
message: "User of Token " + tokenDB.id + " logged out.", message: "User of Token " + tokenDB.id + " logged out.",
body: { loggedOut: true }, body: { loggedOut: true },
type: ["debug", "infoCyan"], type: ["debug", "infoCyan"],
cookie: "",
}) })
}, },
true true

View file

@ -1,104 +1,222 @@
import BurgerMenu from "../../components/BurgerMenu" import BurgerMenu from "../../components/BurgerMenu"
import Logo from "../../components/Logo" import Logo from "../../components/Logo"
import OptionButton from "../../components/OptionButton" import OptionButton from "../../components/OptionButton"
import { gameContext } from "../_app"
import { faEye, faLeftLong } from "@fortawesome/pro-regular-svg-icons" import { faEye, faLeftLong } from "@fortawesome/pro-regular-svg-icons"
import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons" import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons"
import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons" import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import getAccessToken from "@lib/frontend/getAccessToken" import getAccessToken, {
successfulResponse,
} from "@lib/frontend/getAccessToken"
import { GetServerSideProps } from "next" import { GetServerSideProps } from "next"
import { useRouter } from "next/router" 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" import { z } from "zod"
interface Props { 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 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 ( return (
<div className="h-full bg-theme"> <div className="h-full bg-theme">
<div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly"> <div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
<Logo /> <Logo />
<BurgerMenu /> <BurgerMenu />
{!start ? ( {(() => {
<> switch (q) {
<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]"> case "join":
<FontAwesomeIcon return (
className="text-6xl sm:text-7xl md:text-8xl" <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">
icon={faCirclePlay} <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"
</div> onClick={() =>
<button setTimeout(() => {
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" // Navigate to the same page with the `start` query parameter set to `false`
onClick={() => router.push({
setTimeout(() => { pathname: router.pathname,
// Navigate to the same page with the `start` query parameter set to `true` query: null,
router.push({ })
pathname: router.pathname, }, 200)
query: { start: true }, }
}) >
}, 200) <FontAwesomeIcon icon={faLeftLong} />
} </button>
> <div className="flex flex-col items-center gap-6 sm:gap-12">
START <OptionButton icon={faPlus}>Raum erstellen</OptionButton>
</button> <OptionButton
</> action={() => {
) : ( router.push({
<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"> pathname: router.pathname,
<button query: { q: "join" },
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(() => { icon={faUserPlus}
// Navigate to the same page with the `start` query parameter set to `false` >
router.push({ <OtpInput
pathname: router.pathname, containerStyle={{ color: "initial" }}
}) value={otp}
}, 200) onChange={setOtp}
} numInputs={4}
> placeholder="0000"
<FontAwesomeIcon icon={faLeftLong} /> renderSeparator={<span>-</span>}
</button> renderInput={(props) => <input {...props} />}
<div className="flex flex-col items-center gap-6 sm:gap-12"> />
<OptionButton </OptionButton>
action={async () => { <OptionButton icon={faEye}>Zuschauen</OptionButton>
const token = await getAccessToken() </div>
const game = await fetch("/api/game/create", { </div>
method: "POST", )
body: JSON.stringify({ token }), case "start":
}) return (
const gameSchema = z.object({ <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">
game: z.object({ <button
id: z.string(), 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"
createdAt: z.date(), onClick={() =>
updatedAt: z.date(), setTimeout(() => {
running: z.boolean(), // Navigate to the same page with the `start` query parameter set to `false`
}), router.push({
}) pathname: router.pathname,
query: null,
const check = gameSchema.safeParse(game) })
}, 200)
if (check.success) { }
const gameData = check.data >
console.log(gameData) <FontAwesomeIcon icon={faLeftLong} />
} else { </button>
console.error(check.error) <div className="flex flex-col items-center gap-6 sm:gap-12">
} <OptionButton action={() => gameFetch()} icon={faPlus}>
Raum erstellen
// const warst = result.game </OptionButton>
<OptionButton
router.push("/dev/lobby") action={() => {
}} router.push({
icon={faPlus} pathname: router.pathname,
> query: { q: "join" },
Raum erstellen })
</OptionButton> }}
<OptionButton icon={faUserPlus}>Raum beitreten</OptionButton> icon={faUserPlus}
<OptionButton icon={faEye}>Zuschauen</OptionButton> >
</div> Raum beitreten
</div> </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>
</div> </div>
) )
@ -107,10 +225,7 @@ export default function Home({ start }: Props) {
export const getServerSideProps: GetServerSideProps<Props> = async ( export const getServerSideProps: GetServerSideProps<Props> = async (
context context
) => { ) => {
const { start } = context.query const { q } = context.query
// Convert the `start` query parameter to a boolean return { props: { q: q ? q : "" } }
const isStart = start === "true"
return { props: { start: isStart } }
} }

View file

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