Implemented simple full-stack logic (unfinished)
This commit is contained in:
parent
a32b40395e
commit
2862f94f1c
19 changed files with 481 additions and 189 deletions
|
@ -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 (
|
||||
|
|
|
@ -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("")}
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface IdToken {
|
|||
type: TokenType
|
||||
}
|
||||
|
||||
const tokenLifetime = {
|
||||
export const tokenLifetime = {
|
||||
REFRESH: 172800,
|
||||
ACCESS: 15,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 : "" } }
|
||||
}
|
||||
|
|
40
leaky-ships/pnpm-lock.yaml
generated
40
leaky-ships/pnpm-lock.yaml
generated
|
@ -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'}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue