SolidJS implementation
This commit is contained in:
parent
6e9485df22
commit
afe1e0426c
105 changed files with 5152 additions and 3314 deletions
15
leaky-ships/.eslintrc.cjs
Normal file
15
leaky-ships/.eslintrc.cjs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module.exports = {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
},
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
extends: [
|
||||||
|
"plugin:solid/typescript",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/consistent-type-imports": "warn",
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"extends": ["next/core-web-vitals", "prettier"],
|
|
||||||
"rules": {
|
|
||||||
"@next/next/no-img-element": "off"
|
|
||||||
}
|
|
||||||
}
|
|
10
leaky-ships/.gitignore
vendored
10
leaky-ships/.gitignore
vendored
|
@ -14,9 +14,9 @@
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# SolidJS
|
||||||
/.next/
|
/.solid/
|
||||||
/out/
|
/dist/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
@ -35,12 +35,8 @@ yarn-error.log*
|
||||||
.env
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# playwright
|
# playwright
|
||||||
/test-results/
|
/test-results/
|
||||||
|
|
|
@ -1,395 +0,0 @@
|
||||||
import {
|
|
||||||
faSquare2,
|
|
||||||
faSquare3,
|
|
||||||
faSquare4,
|
|
||||||
} from "@fortawesome/pro-regular-svg-icons"
|
|
||||||
import {
|
|
||||||
faBroomWide,
|
|
||||||
faCheck,
|
|
||||||
faComments,
|
|
||||||
faEye,
|
|
||||||
faEyeSlash,
|
|
||||||
faFlag,
|
|
||||||
faGlasses,
|
|
||||||
faLock,
|
|
||||||
faPalette,
|
|
||||||
faReply,
|
|
||||||
faRotate,
|
|
||||||
faScribble,
|
|
||||||
faShip,
|
|
||||||
faSparkles,
|
|
||||||
faSwords,
|
|
||||||
faXmark,
|
|
||||||
} from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { useDrawProps } from "@hooks/useDrawProps"
|
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
|
||||||
import useIndex from "@hooks/useIndex"
|
|
||||||
import useShips from "@hooks/useShips"
|
|
||||||
import { socket } from "@lib/socket"
|
|
||||||
import { modes } from "@lib/utils/helpers"
|
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
import { useCallback, useEffect, useMemo } from "react"
|
|
||||||
import { Icons, toast } from "react-toastify"
|
|
||||||
import { EventBarModes, GameSettings } from "../../interfaces/frontend"
|
|
||||||
import Item from "./Item"
|
|
||||||
|
|
||||||
export function setGameSetting(
|
|
||||||
payload: GameSettings,
|
|
||||||
setSetting: (settings: GameSettings) => string | null,
|
|
||||||
full: (payload: GamePropsSchema) => void,
|
|
||||||
) {
|
|
||||||
return () => {
|
|
||||||
const hash = setSetting(payload)
|
|
||||||
socket.emit("gameSetting", payload, (newHash) => {
|
|
||||||
if (newHash === hash) return
|
|
||||||
console.log("hash", hash, newHash)
|
|
||||||
socket.emit("update", full)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function EventBar({ clear }: { clear: () => void }) {
|
|
||||||
const { shouldHide, color } = useDrawProps()
|
|
||||||
const { selfIndex, isActiveIndex, selfUser } = useIndex()
|
|
||||||
const { ships } = useShips()
|
|
||||||
const router = useRouter()
|
|
||||||
const {
|
|
||||||
payload,
|
|
||||||
userStates,
|
|
||||||
menu,
|
|
||||||
mode,
|
|
||||||
setSetting,
|
|
||||||
full,
|
|
||||||
target,
|
|
||||||
setTarget,
|
|
||||||
setTargetPreview,
|
|
||||||
setIsReady,
|
|
||||||
reset,
|
|
||||||
} = useGameProps()
|
|
||||||
const gameSetting = useCallback(
|
|
||||||
(payload: GameSettings) => setGameSetting(payload, setSetting, full),
|
|
||||||
[full, setSetting],
|
|
||||||
)
|
|
||||||
|
|
||||||
const items = useMemo<EventBarModes>(
|
|
||||||
() => ({
|
|
||||||
main: [
|
|
||||||
{
|
|
||||||
icon: "burger-menu",
|
|
||||||
text: "Menu",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "menu" })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
payload?.game?.state === "running"
|
|
||||||
? {
|
|
||||||
icon: faSwords,
|
|
||||||
text: "Attack",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "moves" })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
icon: faShip,
|
|
||||||
text: "Ships",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "moves" })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "pen",
|
|
||||||
text: "Draw",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "draw" })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "gear",
|
|
||||||
text: "Settings",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "settings" })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
menu: [
|
|
||||||
{
|
|
||||||
icon: faFlag,
|
|
||||||
text: "Surrender",
|
|
||||||
iconColor: "darkred",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "surrender" })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
moves:
|
|
||||||
payload?.game?.state === "running"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
icon: "scope",
|
|
||||||
text: "Fire missile",
|
|
||||||
enabled: mode === 0,
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ mode: 0 })
|
|
||||||
setTarget((e) => ({ ...e, show: false }))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "torpedo",
|
|
||||||
text: "Fire torpedo",
|
|
||||||
enabled: mode === 1 || mode === 2,
|
|
||||||
amount:
|
|
||||||
2 -
|
|
||||||
((selfUser?.moves ?? []).filter(
|
|
||||||
(e) => e.type === "htorpedo" || e.type === "vtorpedo",
|
|
||||||
).length ?? 0),
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ mode: 1 })
|
|
||||||
setTarget((e) => ({ ...e, show: false }))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "radar",
|
|
||||||
text: "Radar scan",
|
|
||||||
enabled: mode === 3,
|
|
||||||
amount:
|
|
||||||
1 -
|
|
||||||
((selfUser?.moves ?? []).filter((e) => e.type === "radar")
|
|
||||||
.length ?? 0),
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ mode: 3 })
|
|
||||||
setTarget((e) => ({ ...e, show: false }))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
icon: faSquare2,
|
|
||||||
text: "Minensucher",
|
|
||||||
amount: 1 - ships.filter((e) => e.size === 2).length,
|
|
||||||
callback: () => {
|
|
||||||
if (1 - ships.filter((e) => e.size === 2).length === 0) return
|
|
||||||
useGameProps.setState({ mode: 0 })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faSquare3,
|
|
||||||
text: "Kreuzer",
|
|
||||||
amount: 3 - ships.filter((e) => e.size === 3).length,
|
|
||||||
callback: () => {
|
|
||||||
if (3 - ships.filter((e) => e.size === 3).length === 0) return
|
|
||||||
useGameProps.setState({ mode: 1 })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faSquare4,
|
|
||||||
text: "Schlachtschiff",
|
|
||||||
amount: 2 - ships.filter((e) => e.size === 4).length,
|
|
||||||
callback: () => {
|
|
||||||
if (2 - ships.filter((e) => e.size === 4).length === 0) return
|
|
||||||
useGameProps.setState({ mode: 2 })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faRotate,
|
|
||||||
text: "Rotate",
|
|
||||||
callback: () => {
|
|
||||||
setTargetPreview((t) => ({
|
|
||||||
...t,
|
|
||||||
orientation: t.orientation === "h" ? "v" : "h",
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
draw: [
|
|
||||||
{ icon: faBroomWide, text: "Clear", callback: clear },
|
|
||||||
{ icon: faPalette, text: "Color", iconColor: color },
|
|
||||||
{
|
|
||||||
icon: shouldHide ? faEye : faEyeSlash,
|
|
||||||
text: shouldHide ? "Show" : "Hide",
|
|
||||||
callback: () => {
|
|
||||||
useDrawProps.setState({ shouldHide: !shouldHide })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
settings: [
|
|
||||||
{
|
|
||||||
icon: faGlasses,
|
|
||||||
text: "Spectators",
|
|
||||||
disabled: !payload?.game?.allowSpectators,
|
|
||||||
callback: gameSetting({
|
|
||||||
allowSpectators: !payload?.game?.allowSpectators,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faSparkles,
|
|
||||||
text: "Specials",
|
|
||||||
disabled: !payload?.game?.allowSpecials,
|
|
||||||
callback: gameSetting({
|
|
||||||
allowSpecials: !payload?.game?.allowSpecials,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faComments,
|
|
||||||
text: "Chat",
|
|
||||||
disabled: !payload?.game?.allowChat,
|
|
||||||
callback: gameSetting({ allowChat: !payload?.game?.allowChat }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faScribble,
|
|
||||||
text: "Mark/Draw",
|
|
||||||
disabled: !payload?.game?.allowMarkDraw,
|
|
||||||
callback: gameSetting({
|
|
||||||
allowMarkDraw: !payload?.game?.allowMarkDraw,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
surrender: [
|
|
||||||
{
|
|
||||||
icon: faCheck,
|
|
||||||
text: "Yes",
|
|
||||||
iconColor: "green",
|
|
||||||
callback: async () => {
|
|
||||||
socket.emit("gameState", "aborted")
|
|
||||||
await router.push("/")
|
|
||||||
reset()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faXmark,
|
|
||||||
text: "No",
|
|
||||||
iconColor: "red",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "main" })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
payload?.game?.state,
|
|
||||||
payload?.game?.allowSpectators,
|
|
||||||
payload?.game?.allowSpecials,
|
|
||||||
payload?.game?.allowChat,
|
|
||||||
payload?.game?.allowMarkDraw,
|
|
||||||
mode,
|
|
||||||
selfUser?.moves,
|
|
||||||
ships,
|
|
||||||
clear,
|
|
||||||
color,
|
|
||||||
shouldHide,
|
|
||||||
gameSetting,
|
|
||||||
setTarget,
|
|
||||||
setTargetPreview,
|
|
||||||
router,
|
|
||||||
reset,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
menu !== "moves" ||
|
|
||||||
payload?.game?.state !== "starting" ||
|
|
||||||
mode < 0 ||
|
|
||||||
items.moves[mode].amount
|
|
||||||
)
|
|
||||||
return
|
|
||||||
const index = items.moves.findIndex((e) => e.amount)
|
|
||||||
useGameProps.setState({ mode: index })
|
|
||||||
}, [items.moves, menu, mode, payload?.game?.state])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
useDrawProps.setState({ enable: menu === "draw" })
|
|
||||||
}, [menu])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (payload?.game?.state !== "running") return
|
|
||||||
|
|
||||||
let toastId = "otherPlayer"
|
|
||||||
if (isActiveIndex) toast.dismiss(toastId)
|
|
||||||
else
|
|
||||||
toast.info("Waiting for other player...", {
|
|
||||||
toastId,
|
|
||||||
position: "top-right",
|
|
||||||
icon: Icons.spinner(),
|
|
||||||
autoClose: false,
|
|
||||||
hideProgressBar: true,
|
|
||||||
closeButton: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// toastId = "connect_error"
|
|
||||||
// const isActive = toast.isActive(toastId)
|
|
||||||
// console.log(toastId, isActive)
|
|
||||||
// if (isActive)
|
|
||||||
// toast.update(toastId, {
|
|
||||||
// autoClose: 5000,
|
|
||||||
// })
|
|
||||||
// else
|
|
||||||
// toast.warn("Spie", { toastId })
|
|
||||||
}, [isActiveIndex, menu, payload?.game?.state])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="event-bar">
|
|
||||||
{menu !== "main" ? (
|
|
||||||
<Item
|
|
||||||
props={{
|
|
||||||
icon: faReply,
|
|
||||||
text: "Return",
|
|
||||||
iconColor: "#555",
|
|
||||||
callback: () => {
|
|
||||||
useGameProps.setState({ menu: "main" })
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{items[menu].map((e, i) => {
|
|
||||||
if (!isActiveIndex && menu === "main" && i === 1) return
|
|
||||||
return <Item key={i} props={e} />
|
|
||||||
})}
|
|
||||||
{menu === "moves" ? (
|
|
||||||
<Item
|
|
||||||
props={{
|
|
||||||
icon:
|
|
||||||
selfIndex >= 0 && userStates[selfIndex].isReady
|
|
||||||
? faLock
|
|
||||||
: faCheck,
|
|
||||||
text:
|
|
||||||
selfIndex >= 0 && userStates[selfIndex].isReady
|
|
||||||
? "unready"
|
|
||||||
: "Done",
|
|
||||||
disabled:
|
|
||||||
payload?.game?.state === "starting" ? mode >= 0 : undefined,
|
|
||||||
enabled:
|
|
||||||
payload?.game?.state === "running" && mode >= 0 && target.show,
|
|
||||||
callback: () => {
|
|
||||||
if (selfIndex < 0) return
|
|
||||||
switch (payload?.game?.state) {
|
|
||||||
case "starting":
|
|
||||||
const isReady = !userStates[selfIndex].isReady
|
|
||||||
setIsReady({ isReady, i: selfIndex })
|
|
||||||
socket.emit("isReady", isReady)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "running":
|
|
||||||
const i = (selfUser?.moves ?? [])
|
|
||||||
.map((e) => e.index)
|
|
||||||
.reduce((prev, curr) => (curr > prev ? curr : prev), 0)
|
|
||||||
const props = {
|
|
||||||
type: modes[mode].type,
|
|
||||||
x: target.x,
|
|
||||||
y: target.y,
|
|
||||||
orientation: target.orientation,
|
|
||||||
index: (selfUser?.moves ?? []).length ? i + 1 : 0,
|
|
||||||
}
|
|
||||||
socket.emit("dispatchMove", props)
|
|
||||||
setTarget((t) => ({ ...t, show: false }))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EventBar
|
|
|
@ -1,13 +0,0 @@
|
||||||
function FogImages() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<img className="fog left" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
|
||||||
<img className="fog right" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
|
||||||
<img className="fog top" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
|
||||||
<img className="fog bottom" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
|
||||||
<img className="fog middle" src={`/fog/fog4.png`} alt={`fog4.png`} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FogImages
|
|
|
@ -1,143 +0,0 @@
|
||||||
import {
|
|
||||||
faRightFromBracket,
|
|
||||||
faSpinnerThird,
|
|
||||||
} from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
|
||||||
import useSocket from "@hooks/useSocket"
|
|
||||||
import { socket } from "@lib/socket"
|
|
||||||
import { useSession } from "next-auth/react"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
import { Fragment, ReactNode, useEffect, useMemo, useState } from "react"
|
|
||||||
import Button from "./Button"
|
|
||||||
import Icon from "./Icon"
|
|
||||||
import Player from "./Player"
|
|
||||||
|
|
||||||
function WithDots({ children }: { children: ReactNode }) {
|
|
||||||
const [dots, setDots] = useState(1)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => setDots((e) => (e % 3) + 1), 1000)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children + " "}
|
|
||||||
{Array.from(Array(dots), () => ".").join("")}
|
|
||||||
{Array.from(Array(3 - dots), (_, i) => (
|
|
||||||
<Fragment key={i}> </Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
|
|
||||||
const { payload, userStates, full, leave, reset } = useGameProps()
|
|
||||||
const { isConnected } = useSocket()
|
|
||||||
const router = useRouter()
|
|
||||||
const { data: session } = useSession()
|
|
||||||
const [launchTime, setLaunchTime] = useState(3)
|
|
||||||
|
|
||||||
const launching = useMemo(
|
|
||||||
() =>
|
|
||||||
payload?.users.length === 2 &&
|
|
||||||
!userStates.filter((user) => !user.isReady).length,
|
|
||||||
[payload?.users.length, userStates],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!launching || launchTime > 0) return
|
|
||||||
socket.emit("gameState", "starting")
|
|
||||||
}, [launching, launchTime, router])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!launching) return setLaunchTime(3)
|
|
||||||
if (launchTime < 0) return
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setLaunchTime((e) => e - 1)
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => clearTimeout(timeout)
|
|
||||||
}, [launching, launchTime])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (payload?.game?.id || !isConnected) return
|
|
||||||
socket.emit("update", full)
|
|
||||||
}, [full, payload?.game?.id, isConnected])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
typeof payload?.game?.state !== "string" ||
|
|
||||||
payload?.game?.state === "lobby"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
router.push("/gamefield")
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-32 flex flex-col self-stretch rounded-3xl bg-gray-400">
|
|
||||||
<div className="flex items-center justify-between border-b-2 border-slate-900">
|
|
||||||
<Icon src="speech_bubble.png">Chat</Icon>
|
|
||||||
<h1 className="font-farro text-5xl font-medium">
|
|
||||||
{launching ? (
|
|
||||||
<WithDots>
|
|
||||||
{launchTime < 0
|
|
||||||
? "Game starts"
|
|
||||||
: "Game is starting in " + launchTime}
|
|
||||||
</WithDots>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{"Game-PIN: "}
|
|
||||||
{isConnected ? (
|
|
||||||
<span className="underline">{payload?.gamePin ?? "----"}</span>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon icon={faSpinnerThird} spin={true} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</h1>
|
|
||||||
<Icon src="gear.png" onClick={openSettings}>
|
|
||||||
Settings
|
|
||||||
</Icon>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-around">
|
|
||||||
{isConnected ? (
|
|
||||||
<>
|
|
||||||
<Player src="player_blue.png" i={0} userId={session?.user.id} />
|
|
||||||
<p className="font-farro m-4 text-6xl font-semibold">VS</p>
|
|
||||||
{payload?.users[1] ? (
|
|
||||||
<Player src="player_red.png" i={1} userId={session?.user.id} />
|
|
||||||
) : (
|
|
||||||
<p className="font-farro w-96 text-center text-4xl font-medium">
|
|
||||||
<WithDots>Warte auf Spieler 2</WithDots>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="font-farro m-48 text-center text-6xl font-medium">
|
|
||||||
Warte auf Verbindung
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-around border-t-2 border-slate-900 p-4">
|
|
||||||
<Button
|
|
||||||
type={launching ? "gray" : "red"}
|
|
||||||
disabled={launching}
|
|
||||||
onClick={() => {
|
|
||||||
leave(async () => {
|
|
||||||
reset()
|
|
||||||
await router.push("/")
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>LEAVE</span>
|
|
||||||
<FontAwesomeIcon icon={faRightFromBracket} className="ml-4 w-12" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LobbyFrame
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect, test, type BrowserContext, type Page } from "@playwright/test"
|
import { expect, test, type BrowserContext, type Page } from "@playwright/test"
|
||||||
|
|
||||||
const callbackUrl = process.env.NEXTAUTH_URL + "/"
|
const callbackUrl = process.env.AUTH_URL + "/"
|
||||||
let context: BrowserContext
|
let context: BrowserContext
|
||||||
let page: Page
|
let page: Page
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
type Page,
|
type Page,
|
||||||
} from "@playwright/test"
|
} from "@playwright/test"
|
||||||
import { createHash, randomBytes } from "crypto"
|
import { createHash, randomBytes } from "crypto"
|
||||||
import prisma from "../lib/prisma"
|
import prisma from "~/lib/prisma"
|
||||||
|
|
||||||
const callbackUrl = process.env.NEXTAUTH_URL + "/"
|
const callbackUrl = process.env.NEXTAUTH_URL + "/"
|
||||||
const player1Email = (browser: Browser) =>
|
const player1Email = (browser: Browser) =>
|
||||||
|
@ -43,7 +43,7 @@ test.describe.serial("Check Email auth", () => {
|
||||||
|
|
||||||
const hash = createHash("sha256")
|
const hash = createHash("sha256")
|
||||||
// Prefer provider specific secret, but use default secret if none specified
|
// Prefer provider specific secret, but use default secret if none specified
|
||||||
.update(`${token}${process.env.NEXTAUTH_SECRET}`)
|
.update(`${token}${process.env.AUTH_SECRET}`)
|
||||||
.digest("hex")
|
.digest("hex")
|
||||||
|
|
||||||
// Use Prisma to fetch the latest token for the email
|
// Use Prisma to fetch the latest token for the email
|
||||||
|
|
1
leaky-ships/global.d.ts
vendored
1
leaky-ships/global.d.ts
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
import { PrismaClient } from "@prisma/client"
|
||||||
import "@total-typescript/ts-reset"
|
import "@total-typescript/ts-reset"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { produce } from "immer"
|
|
||||||
import { create } from "zustand"
|
|
||||||
import { devtools } from "zustand/middleware"
|
|
||||||
|
|
||||||
const initialState: {
|
|
||||||
enable: boolean
|
|
||||||
shouldHide: boolean
|
|
||||||
color: string
|
|
||||||
} = {
|
|
||||||
enable: false,
|
|
||||||
shouldHide: false,
|
|
||||||
color: "#b32aa9",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type State = typeof initialState
|
|
||||||
|
|
||||||
export type Action = {
|
|
||||||
setColor: (color: string) => void
|
|
||||||
reset: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDrawProps = create<State & Action>()(
|
|
||||||
devtools(
|
|
||||||
(set) => ({
|
|
||||||
...initialState,
|
|
||||||
setColor: (color) =>
|
|
||||||
set(
|
|
||||||
produce((state) => {
|
|
||||||
state.color = color
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
reset: () => {
|
|
||||||
set(initialState)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "gameState",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,274 +0,0 @@
|
||||||
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
|
|
||||||
import { socket } from "@lib/socket"
|
|
||||||
import {
|
|
||||||
initlialMouseCursor,
|
|
||||||
initlialTarget,
|
|
||||||
initlialTargetPreview,
|
|
||||||
intersectingShip,
|
|
||||||
targetList,
|
|
||||||
} from "@lib/utils/helpers"
|
|
||||||
import {
|
|
||||||
GamePropsSchema,
|
|
||||||
optionalGamePropsSchema,
|
|
||||||
PlayerSchema,
|
|
||||||
} from "@lib/zodSchemas"
|
|
||||||
import { GameState, MoveType } from "@prisma/client"
|
|
||||||
import { produce } from "immer"
|
|
||||||
import { SetStateAction } from "react"
|
|
||||||
import { toast } from "react-toastify"
|
|
||||||
import { create } from "zustand"
|
|
||||||
import { devtools } from "zustand/middleware"
|
|
||||||
import {
|
|
||||||
EventBarModes,
|
|
||||||
GameSettings,
|
|
||||||
MouseCursor,
|
|
||||||
MoveDispatchProps,
|
|
||||||
ShipProps,
|
|
||||||
Target,
|
|
||||||
TargetPreview,
|
|
||||||
} from "../interfaces/frontend"
|
|
||||||
|
|
||||||
const initialState: optionalGamePropsSchema & {
|
|
||||||
userStates: {
|
|
||||||
isReady: boolean
|
|
||||||
isConnected: boolean
|
|
||||||
}[]
|
|
||||||
menu: keyof EventBarModes
|
|
||||||
mode: number
|
|
||||||
target: Target
|
|
||||||
targetPreview: TargetPreview
|
|
||||||
mouseCursor: MouseCursor
|
|
||||||
} = {
|
|
||||||
menu: "moves",
|
|
||||||
mode: 0,
|
|
||||||
payload: null,
|
|
||||||
hash: null,
|
|
||||||
target: initlialTarget,
|
|
||||||
targetPreview: initlialTargetPreview,
|
|
||||||
mouseCursor: initlialMouseCursor,
|
|
||||||
userStates: Array.from(Array(2), () => ({
|
|
||||||
isReady: false,
|
|
||||||
isConnected: false,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
|
|
||||||
export type State = typeof initialState
|
|
||||||
|
|
||||||
export type Action = {
|
|
||||||
DispatchMove: (props: MoveDispatchProps, i: number) => void
|
|
||||||
setTarget: (target: SetStateAction<Target>) => void
|
|
||||||
setTargetPreview: (targetPreview: SetStateAction<TargetPreview>) => void
|
|
||||||
setMouseCursor: (mouseCursor: SetStateAction<MouseCursor>) => void
|
|
||||||
setPlayer: (payload: { users: PlayerSchema[] }) => string | null
|
|
||||||
setSetting: (settings: GameSettings) => string | null
|
|
||||||
full: (newProps: GamePropsSchema) => void
|
|
||||||
leave: (cb: () => void) => void
|
|
||||||
setIsReady: (payload: { i: number; isReady: boolean }) => void
|
|
||||||
gameState: (newState: GameState) => void
|
|
||||||
setShips: (ships: ShipProps[], index: number) => void
|
|
||||||
removeShip: (props: ShipProps, index: number) => void
|
|
||||||
setIsConnected: (payload: { i: number; isConnected: boolean }) => void
|
|
||||||
reset: () => void
|
|
||||||
setActiveIndex: (i: number, selfIndex: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGameProps = create<State & Action>()(
|
|
||||||
devtools(
|
|
||||||
(set) => ({
|
|
||||||
...initialState,
|
|
||||||
setActiveIndex: (i, selfIndex) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (!state.payload) return
|
|
||||||
state.payload.activeIndex = i
|
|
||||||
if (i === selfIndex) {
|
|
||||||
state.menu = "moves"
|
|
||||||
state.mode = 0
|
|
||||||
} else {
|
|
||||||
state.menu = "main"
|
|
||||||
state.mode = -1
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
DispatchMove: (move, i) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (!state.payload) return
|
|
||||||
const list = targetList(move, move.type)
|
|
||||||
state.payload.users.map((e) => {
|
|
||||||
if (!e) return e
|
|
||||||
if (i === e.index) e.moves.push(move)
|
|
||||||
else if (move.type !== MoveType.radar)
|
|
||||||
e.hits.push(
|
|
||||||
...list.map(({ x, y }) => ({
|
|
||||||
hit: !!intersectingShip(e.ships, {
|
|
||||||
...move,
|
|
||||||
size: 1,
|
|
||||||
variant: 0,
|
|
||||||
}).fields.length,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setTarget: (dispatch) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (typeof dispatch === "function")
|
|
||||||
state.target = dispatch(state.target)
|
|
||||||
else state.target = dispatch
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setTargetPreview: (dispatch) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (typeof dispatch === "function")
|
|
||||||
state.targetPreview = dispatch(state.targetPreview)
|
|
||||||
else state.targetPreview = dispatch
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setMouseCursor: (dispatch) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (typeof dispatch === "function")
|
|
||||||
state.mouseCursor = dispatch(state.mouseCursor)
|
|
||||||
else state.mouseCursor = dispatch
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setShips: (ships, index) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (!state.payload) return
|
|
||||||
state.payload.users = state.payload.users.map((e) => {
|
|
||||||
if (!e || e.index !== index) return e
|
|
||||||
e.ships = ships
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
removeShip: ({ size, variant, x, y }, index) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
state.payload?.users.map((e) => {
|
|
||||||
if (!e || e.index !== index) return
|
|
||||||
const indexToRemove = e.ships.findIndex(
|
|
||||||
(ship) =>
|
|
||||||
ship.size === size &&
|
|
||||||
ship.variant === variant &&
|
|
||||||
ship.x === x &&
|
|
||||||
ship.y === y,
|
|
||||||
)
|
|
||||||
e.ships.splice(indexToRemove, 1)
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setPlayer: (payload) => {
|
|
||||||
let hash: string | null = null
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (!state.payload) return
|
|
||||||
state.payload.users = payload.users
|
|
||||||
const body = getPayloadwithChecksum(state.payload)
|
|
||||||
if (!body.hash) {
|
|
||||||
toast.warn("Something is wrong... ", {
|
|
||||||
toastId: "st_wrong",
|
|
||||||
theme: "colored",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hash = body.hash
|
|
||||||
state.hash = hash
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return hash
|
|
||||||
},
|
|
||||||
setSetting: (settings) => {
|
|
||||||
let hash: string | null = null
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (!state.payload?.game) return
|
|
||||||
Object.assign(state.payload.game, settings)
|
|
||||||
const body = getPayloadwithChecksum(state.payload)
|
|
||||||
if (!body.hash) {
|
|
||||||
toast.warn("Something is wrong... ", {
|
|
||||||
toastId: "st_wrong",
|
|
||||||
theme: "colored",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hash = body.hash
|
|
||||||
state.hash = hash
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return hash
|
|
||||||
},
|
|
||||||
full: (newGameProps) =>
|
|
||||||
set((state) => {
|
|
||||||
if (state.hash === newGameProps.hash) {
|
|
||||||
console.log("Everything up to date.")
|
|
||||||
} else {
|
|
||||||
console.log("Update was needed.", state.hash, newGameProps.hash)
|
|
||||||
|
|
||||||
if (
|
|
||||||
state.payload?.game?.id &&
|
|
||||||
state.payload?.game?.id !== newGameProps.payload?.game?.id
|
|
||||||
) {
|
|
||||||
console.warn(
|
|
||||||
"Different gameId detected on update: ",
|
|
||||||
state.payload?.game?.id,
|
|
||||||
newGameProps.payload?.game?.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newGameProps
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}),
|
|
||||||
leave: (cb) => {
|
|
||||||
socket.emit("leave", (ack) => {
|
|
||||||
if (!ack) {
|
|
||||||
toast.error("Something is wrong...")
|
|
||||||
}
|
|
||||||
cb()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setIsReady: ({ i, isReady }) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
state.userStates[i].isReady = isReady
|
|
||||||
state.userStates[i].isConnected = true
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
gameState: (newState: GameState) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
if (!state.payload?.game) return
|
|
||||||
state.payload.game.state = newState
|
|
||||||
state.userStates = state.userStates.map((e) => ({
|
|
||||||
...e,
|
|
||||||
isReady: false,
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setIsConnected: ({ i, isConnected }) =>
|
|
||||||
set(
|
|
||||||
produce((state: State) => {
|
|
||||||
state.userStates[i].isConnected = isConnected
|
|
||||||
if (isConnected) return
|
|
||||||
state.userStates[i].isReady = false
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
reset: () => {
|
|
||||||
set(initialState)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "gameState",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import logging, { Logging } from "./logging"
|
|
||||||
|
|
||||||
export interface Result<T> {
|
|
||||||
message: string
|
|
||||||
statusCode?: number
|
|
||||||
body?: T
|
|
||||||
type?: Logging[]
|
|
||||||
redirectUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function sendResponse<T>(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<T>,
|
|
||||||
result: Result<T>,
|
|
||||||
) {
|
|
||||||
if (result.redirectUrl) {
|
|
||||||
res.redirect(result.statusCode ?? 307, result.redirectUrl)
|
|
||||||
} else {
|
|
||||||
res.status(result.statusCode ?? 200)
|
|
||||||
result.body ? res.json(result.body) : res.end()
|
|
||||||
logging(result.message, result.type ?? ["debug"], req)
|
|
||||||
}
|
|
||||||
return "done" as const
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
|
|
@ -1,64 +1,62 @@
|
||||||
{
|
{
|
||||||
"name": "leaky-ships",
|
"name": "leaky-ships",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "solid-start dev --port 3000",
|
||||||
"build": "next build",
|
"start": "solid-start start --port 3000",
|
||||||
"start": "next start",
|
"build": "solid-start build",
|
||||||
"lint": "next lint",
|
"lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"",
|
||||||
|
"push": "prisma db push",
|
||||||
|
"postinstall": "prisma generate",
|
||||||
"test": "pnpm playwright test --ui"
|
"test": "pnpm playwright test --ui"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@auth/core": "0.12.0",
|
||||||
"@fortawesome/pro-duotone-svg-icons": "^6.4.0",
|
"@auth/prisma-adapter": "^1.0.1",
|
||||||
"@fortawesome/pro-light-svg-icons": "^6.4.0",
|
"@auth/solid-start": "^0.1.1",
|
||||||
"@fortawesome/pro-regular-svg-icons": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/pro-solid-svg-icons": "^6.4.0",
|
"@fortawesome/pro-duotone-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/pro-thin-svg-icons": "^6.4.0",
|
"@fortawesome/pro-light-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/pro-regular-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/sharp-solid-svg-icons": "^6.4.0",
|
"@fortawesome/pro-solid-svg-icons": "^6.4.2",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@fortawesome/pro-thin-svg-icons": "^6.4.2",
|
||||||
"@next/font": "13.1.1",
|
"@fortawesome/sharp-solid-svg-icons": "^6.4.2",
|
||||||
"@prisma/client": "^4.16.2",
|
"@prisma/client": "^4.16.2",
|
||||||
|
"@solidjs/meta": "^0.28.6",
|
||||||
|
"@solidjs/router": "^0.8.3",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"eslint": "8.31.0",
|
|
||||||
"eslint-config-next": "13.1.1",
|
|
||||||
"http-status": "^1.6.2",
|
"http-status": "^1.6.2",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"next": "13.1.1",
|
"nodemailer": "6.9.4",
|
||||||
"next-auth": "^4.22.3",
|
|
||||||
"nodemailer": "^6.9.4",
|
|
||||||
"prisma": "^4.16.2",
|
"prisma": "^4.16.2",
|
||||||
"react": "18.2.0",
|
"socket.io": "^4.7.2",
|
||||||
"react-colorful": "^5.6.1",
|
"socket.io-client": "^4.7.2",
|
||||||
"react-dom": "18.2.0",
|
"solid-js": "^1.7.11",
|
||||||
"react-otp-input": "^3.0.4",
|
"solid-start": "^0.3.2",
|
||||||
"react-toastify": "^9.1.3",
|
"solid-zustand": "^1.7.0",
|
||||||
"socket.io": "^4.7.1",
|
"typescript": "5.1.6",
|
||||||
"socket.io-client": "^4.7.1",
|
|
||||||
"typescript": "4.9.4",
|
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"zod": "3.21.1",
|
"zod": "3.21.1",
|
||||||
"zod-prisma-types": "^2.7.4",
|
"zod-prisma-types": "^2.7.9"
|
||||||
"zustand": "^4.3.9"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.36.2",
|
"@playwright/test": "^1.37.0",
|
||||||
"@total-typescript/ts-reset": "^0.3.7",
|
"@total-typescript/ts-reset": "^0.3.7",
|
||||||
"@types/node": "^18.17.0",
|
"@types/node": "^18.17.5",
|
||||||
"@types/react": "^18.2.15",
|
"@types/nodemailer": "^6.4.9",
|
||||||
"@types/react-dom": "^18.2.7",
|
|
||||||
"@types/web-bluetooth": "^0.0.16",
|
"@types/web-bluetooth": "^0.0.16",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.15",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
|
"eslint-plugin-solid": "^0.12.1",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.1",
|
||||||
"prettier-plugin-organize-imports": "^3.2.3",
|
"prettier-plugin-organize-imports": "^3.2.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.65.1",
|
||||||
"tailwindcss": "^3.3.3"
|
"solid-start-node": "^0.3.2",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"vite": "^4.4.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import "@fortawesome/fontawesome-svg-core/styles.css"
|
|
||||||
import { SessionProvider } from "next-auth/react"
|
|
||||||
import type { AppProps } from "next/app"
|
|
||||||
import { ToastContainer } from "react-toastify"
|
|
||||||
import "react-toastify/dist/ReactToastify.css"
|
|
||||||
import "../styles/App.scss"
|
|
||||||
import "../styles/globals.scss"
|
|
||||||
import "../styles/grid.scss"
|
|
||||||
import "../styles/grid2.scss"
|
|
||||||
|
|
||||||
export default function App({
|
|
||||||
Component,
|
|
||||||
pageProps: { session, ...pageProps },
|
|
||||||
}: AppProps) {
|
|
||||||
return (
|
|
||||||
<SessionProvider session={session}>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
<ToastContainer />
|
|
||||||
</SessionProvider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { Head, Html, Main, NextScript } from "next/document"
|
|
||||||
|
|
||||||
export default function Document() {
|
|
||||||
return (
|
|
||||||
<Html lang="en">
|
|
||||||
<Head />
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { rejectionErrors } from "@lib/backend/errors"
|
|
||||||
import sendResponse from "@lib/backend/sendResponse"
|
|
||||||
import prisma from "@lib/prisma"
|
|
||||||
import { Game } from "@prisma/client"
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import { getServerSession } from "next-auth"
|
|
||||||
import { authOptions } from "../auth/[...nextauth]"
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
game: Game
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function id(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>,
|
|
||||||
) {
|
|
||||||
const gameId = req.query.id
|
|
||||||
const session = await getServerSession(req, res, authOptions)
|
|
||||||
|
|
||||||
if (!session?.user || typeof gameId !== "string") {
|
|
||||||
return sendResponse(req, res, rejectionErrors.unauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
let game: Game | null
|
|
||||||
switch (req.method) {
|
|
||||||
case "DELETE":
|
|
||||||
game = await prisma.game.delete({
|
|
||||||
where: { id: gameId },
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
game = await prisma.game.findFirst({
|
|
||||||
where: { id: gameId },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!game) {
|
|
||||||
return sendResponse(req, res, rejectionErrors.gameNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendResponse(req, res, {
|
|
||||||
message: "Here is the game.",
|
|
||||||
body: { game },
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
|
||||||
import { useSession } from "next-auth/react"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
import { toast } from "react-toastify"
|
|
||||||
|
|
||||||
export default function Game() {
|
|
||||||
const { payload } = useGameProps()
|
|
||||||
const router = useRouter()
|
|
||||||
const { data: session } = useSession()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const gameId = payload?.game?.id
|
|
||||||
const path = gameId ? "/game" : "/start"
|
|
||||||
toast.promise(router.push(path), {
|
|
||||||
pending: {
|
|
||||||
render: "Wird weitergeleitet...",
|
|
||||||
toastId: "pageLoad",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
render: gameId
|
|
||||||
? "Spiel gefunden!"
|
|
||||||
: session?.user.id
|
|
||||||
? "Kein laufendes Spiel."
|
|
||||||
: "Kein laufendes Spiel. Bitte anmelden.",
|
|
||||||
toastId: "pageLoad",
|
|
||||||
theme: session?.user.id ? "dark" : undefined,
|
|
||||||
type: gameId ? "success" : "info",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
render: "Es ist ein Fehler aufgetreten 🤯",
|
|
||||||
type: "error",
|
|
||||||
toastId: "pageLoad",
|
|
||||||
theme: "colored",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full bg-theme">
|
|
||||||
<div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import Gamefield from "@components/Gamefield/Gamefield"
|
|
||||||
import Head from "next/head"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Create Next App</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<Gamefield />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import Grid from "@components/Grid"
|
|
||||||
import Head from "next/head"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Create Next App</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<Grid />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import Grid2 from "@components/Grid2"
|
|
||||||
import Head from "next/head"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Create Next App</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<Grid2 />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import BurgerMenu from "@components/BurgerMenu"
|
|
||||||
import Logo from "@components/Logo"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
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 />
|
|
||||||
<div className="flex h-36 w-64 items-center justify-center overflow-hidden rounded-xl border-8 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]">
|
|
||||||
<video controls preload="metadata" src="/Regelwerk.mp4" />
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
id="start"
|
|
||||||
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(() => {
|
|
||||||
router.push("/start")
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
START
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
import { faLeftLong } from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { signIn, useSession } from "next-auth/react"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
import { FormEvent, useEffect, useState } from "react"
|
|
||||||
import { toast } from "react-toastify"
|
|
||||||
|
|
||||||
type SignInErrorTypes =
|
|
||||||
| "Signin"
|
|
||||||
| "OAuthSignin"
|
|
||||||
| "OAuthCallback"
|
|
||||||
| "OAuthCreateAccount"
|
|
||||||
| "EmailCreateAccount"
|
|
||||||
| "Callback"
|
|
||||||
| "OAuthAccountNotLinked"
|
|
||||||
| "EmailSignin"
|
|
||||||
| "CredentialsSignin"
|
|
||||||
| "SessionRequired"
|
|
||||||
| "default"
|
|
||||||
|
|
||||||
const errors: Record<SignInErrorTypes, string> = {
|
|
||||||
Signin: "Try signing in with a different account.",
|
|
||||||
OAuthSignin: "Try signing in with a different account.",
|
|
||||||
OAuthCallback: "Try signing in with a different account.",
|
|
||||||
OAuthCreateAccount: "Try signing in with a different account.",
|
|
||||||
EmailCreateAccount: "Try signing in with a different account.",
|
|
||||||
Callback: "Try signing in with a different account.",
|
|
||||||
OAuthAccountNotLinked:
|
|
||||||
"To confirm your identity, sign in with the same account you used originally.",
|
|
||||||
EmailSignin: "The e-mail could not be sent.",
|
|
||||||
CredentialsSignin:
|
|
||||||
"Sign in failed. Check the details you provided are correct.",
|
|
||||||
SessionRequired: "Please sign in to access this page.",
|
|
||||||
default: "Unable to sign in.",
|
|
||||||
}
|
|
||||||
|
|
||||||
function Login() {
|
|
||||||
const [email, setEmail] = useState("")
|
|
||||||
const { status } = useSession()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const errorType = router.query.error as SignInErrorTypes
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!errorType) return
|
|
||||||
toast.error(errors[errorType] ?? errors.default, { theme: "colored" })
|
|
||||||
}, [errorType])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === "authenticated") router.push("/")
|
|
||||||
}, [router, status])
|
|
||||||
|
|
||||||
function login(provider: "email" | "azure-ad") {
|
|
||||||
return (e?: FormEvent) => {
|
|
||||||
e?.preventDefault()
|
|
||||||
signIn(provider, { email, callbackUrl: "/" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat">
|
|
||||||
<div className="rounded-xl bg-gray-800 bg-opacity-60 px-16 py-10 text-white shadow-lg backdrop-blur-md max-sm:px-8">
|
|
||||||
<div className="mb-8 flex flex-col items-center">
|
|
||||||
<img
|
|
||||||
className="rounded-full shadow-lg"
|
|
||||||
src="/logo512.png"
|
|
||||||
width="150"
|
|
||||||
alt="Avatar"
|
|
||||||
/>
|
|
||||||
<h1 className="mb-2 text-2xl">Leaky Ships</h1>
|
|
||||||
<span className="text-gray-300">Choose Login Method</span>
|
|
||||||
</div>
|
|
||||||
{errorType && <hr className="mb-8 border-gray-400" />}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<form className="flex flex-col" onSubmit={login("email")}>
|
|
||||||
<label htmlFor="email" className="mx-2 text-lg">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="my-1 rounded-lg border-2 border-gray-500 bg-slate-800 bg-opacity-60 px-6 py-2 text-center text-inherit placeholder-slate-400 shadow-lg outline-none backdrop-blur-md focus-within:border-blue-500"
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
id="email"
|
|
||||||
placeholder="user@example.com"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
id="email-submit"
|
|
||||||
type="submit"
|
|
||||||
className="my-1 rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Sign in with Email
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<hr className="w-full" />
|
|
||||||
<span className="mx-4 my-2">or</span>
|
|
||||||
<hr className="w-full" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-2 flex flex-col rounded-lg bg-gradient-to-tr from-[#fff8] via-[#fffd] to-[#fff8] p-4 shadow-lg drop-shadow-md">
|
|
||||||
<a
|
|
||||||
href="https://gbs-grafschaft.de/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/images/logo-gbs.png"
|
|
||||||
loading="lazy"
|
|
||||||
alt="Gewerbliche Berufsbildende Schulen"
|
|
||||||
className="m-4 mt-2 w-60 justify-center"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
id="microsoft"
|
|
||||||
onClick={login("azure-ad")}
|
|
||||||
className="flex w-full justify-evenly rounded-lg border border-gray-400 bg-slate-100 px-5 py-3 text-black drop-shadow-md duration-300 hover:bg-slate-200"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/images/Microsoft_icon.svg"
|
|
||||||
loading="lazy"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
alt="Microsoft_icon"
|
|
||||||
/>
|
|
||||||
<span>Sign in with Microsoft</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{errorType ? (
|
|
||||||
<>
|
|
||||||
<hr className="mt-8 border-gray-400" />
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<button
|
|
||||||
id="back"
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="mt-10 rounded-lg border-2 border-gray-400 bg-gray-500 bg-opacity-75 px-16 py-2 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:border-blue-600"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faLeftLong} />
|
|
||||||
<span className="mx-4 font-bold">Return</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Login
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { signOut, useSession } from "next-auth/react"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
|
|
||||||
function Logout() {
|
|
||||||
const { status } = useSession()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === "unauthenticated") router.push("/signin")
|
|
||||||
}, [router, status])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat">
|
|
||||||
<div className="rounded-xl bg-gray-800 bg-opacity-50 px-16 py-10 shadow-lg backdrop-blur-md max-sm:px-8">
|
|
||||||
<div className="text-white">
|
|
||||||
<div className="mb-8 flex flex-col items-center">
|
|
||||||
<img
|
|
||||||
className="rounded-full shadow-lg"
|
|
||||||
src="/logo512.png"
|
|
||||||
width="150"
|
|
||||||
alt="Avatar"
|
|
||||||
/>
|
|
||||||
<h1 className="mb-2 text-2xl">Leaky Ships</h1>
|
|
||||||
<span className="text-gray-300">Signout</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-start gap-4">
|
|
||||||
<span>Are you sure you want to sign out?</span>
|
|
||||||
<button
|
|
||||||
id="signout"
|
|
||||||
onClick={() => signOut({ callbackUrl: "/" })}
|
|
||||||
className="rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Logout
|
|
|
@ -1,231 +0,0 @@
|
||||||
import BurgerMenu from "@components/BurgerMenu"
|
|
||||||
import Logo from "@components/Logo"
|
|
||||||
import OptionButton from "@components/OptionButton"
|
|
||||||
import { faEye, faLeftLong } from "@fortawesome/pro-regular-svg-icons"
|
|
||||||
import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
|
||||||
import status from "http-status"
|
|
||||||
import { useSession } from "next-auth/react"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
|
||||||
import OtpInput from "react-otp-input"
|
|
||||||
import { Icons, toast } from "react-toastify"
|
|
||||||
|
|
||||||
export function isAuthenticated(res: Response) {
|
|
||||||
switch (status[`${res.status}_CLASS`]) {
|
|
||||||
case status.classes.SUCCESSFUL:
|
|
||||||
case status.classes.REDIRECTION:
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resStatus = status[`${res.status}_CLASS`]
|
|
||||||
if (typeof resStatus !== "string") return
|
|
||||||
|
|
||||||
toast(status[res.status], {
|
|
||||||
position: "top-center",
|
|
||||||
type: "info",
|
|
||||||
theme: "colored",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirmation = () => {
|
|
||||||
const toastId = "confirm"
|
|
||||||
toast.warn(
|
|
||||||
<div id="toast-confirm">
|
|
||||||
<h4>You are already in another round, do you want to:</h4>
|
|
||||||
<button onClick={() => toast.dismiss(toastId)}>Join</button>
|
|
||||||
or
|
|
||||||
<button onClick={() => toast.dismiss(toastId)}>Leave</button>
|
|
||||||
</div>,
|
|
||||||
{ autoClose: false, toastId },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Start() {
|
|
||||||
const [otp, setOtp] = useState("")
|
|
||||||
const { full } = useGameProps()
|
|
||||||
const router = useRouter()
|
|
||||||
const { data: session } = useSession()
|
|
||||||
|
|
||||||
const query = useMemo((): { join?: boolean; watch?: boolean } => {
|
|
||||||
switch (router.query.q) {
|
|
||||||
case "join":
|
|
||||||
return { join: true }
|
|
||||||
case "watch":
|
|
||||||
return { watch: true }
|
|
||||||
default:
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}, [router])
|
|
||||||
|
|
||||||
const gameFetch = useCallback(
|
|
||||||
async (pin?: string) => {
|
|
||||||
const gameRequestPromise = fetch(
|
|
||||||
"/api/game/" + (!pin ? "create" : "join"),
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ pin }),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(isAuthenticated)
|
|
||||||
.then((game) => GamePropsSchema.parse(game))
|
|
||||||
|
|
||||||
const move = !pin ? "erstellt" : "angefragt"
|
|
||||||
const toastId = "pageLoad"
|
|
||||||
toast("Raum wird " + move, {
|
|
||||||
icon: Icons.spinner(),
|
|
||||||
toastId,
|
|
||||||
autoClose: false,
|
|
||||||
hideProgressBar: true,
|
|
||||||
closeButton: false,
|
|
||||||
})
|
|
||||||
const res = await gameRequestPromise.catch(() =>
|
|
||||||
toast.update(toastId, {
|
|
||||||
render: "Es ist ein Fehler aufgetreten bei der Anfrage 🤯",
|
|
||||||
type: "error",
|
|
||||||
icon: Icons.error,
|
|
||||||
theme: "colored",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeButton: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (!res) return
|
|
||||||
full(res)
|
|
||||||
|
|
||||||
toast.update(toastId, {
|
|
||||||
render: "Weiterleitung",
|
|
||||||
})
|
|
||||||
|
|
||||||
router
|
|
||||||
.push("/lobby")
|
|
||||||
.then(() =>
|
|
||||||
toast.update(toastId, {
|
|
||||||
render: "Raum begetreten 👌",
|
|
||||||
type: "info",
|
|
||||||
icon: Icons.success,
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeButton: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch(() =>
|
|
||||||
toast.update(toastId, {
|
|
||||||
render: "Es ist ein Fehler aufgetreten beim Seiten wechsel 🤯",
|
|
||||||
type: "error",
|
|
||||||
icon: Icons.error,
|
|
||||||
theme: "colored",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeButton: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[router, full],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (otp.length !== 4) return
|
|
||||||
gameFetch(otp)
|
|
||||||
}, [otp, gameFetch])
|
|
||||||
|
|
||||||
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 />
|
|
||||||
<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">
|
|
||||||
<div className="flex w-full justify-between">
|
|
||||||
<button
|
|
||||||
id="back"
|
|
||||||
className="-mt-2 h-14 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(() => {
|
|
||||||
router.push("/")
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faLeftLong} />
|
|
||||||
</button>
|
|
||||||
{!session?.user.id && (
|
|
||||||
<button
|
|
||||||
id="login"
|
|
||||||
className="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-orange-500 bg-yellow-500 text-2xl active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-4xl"
|
|
||||||
onClick={() =>
|
|
||||||
setTimeout(() => {
|
|
||||||
router.push("/signin")
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center gap-6 sm:gap-12">
|
|
||||||
<OptionButton
|
|
||||||
id="Raum erstellen"
|
|
||||||
callback={() => gameFetch()}
|
|
||||||
icon={faPlus}
|
|
||||||
disabled={!session}
|
|
||||||
/>
|
|
||||||
<OptionButton
|
|
||||||
id="Raum beitreten"
|
|
||||||
callback={() => {
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: { q: "join" },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
icon={faUserPlus}
|
|
||||||
disabled={!session}
|
|
||||||
node={
|
|
||||||
query.join && session ? (
|
|
||||||
<OtpInput
|
|
||||||
shouldAutoFocus
|
|
||||||
containerStyle={{ color: "initial" }}
|
|
||||||
value={otp}
|
|
||||||
onChange={setOtp}
|
|
||||||
numInputs={4}
|
|
||||||
inputType="number"
|
|
||||||
inputStyle="inputStyle"
|
|
||||||
placeholder="0000"
|
|
||||||
renderSeparator={<span>-</span>}
|
|
||||||
renderInput={(props) => <input {...props} />}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<OptionButton
|
|
||||||
id="Zuschauen"
|
|
||||||
icon={faEye}
|
|
||||||
callback={() => {
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: { q: "watch" },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
node={
|
|
||||||
query.watch ? (
|
|
||||||
<OtpInput
|
|
||||||
shouldAutoFocus
|
|
||||||
containerStyle={{ color: "initial" }}
|
|
||||||
value={otp}
|
|
||||||
onChange={setOtp}
|
|
||||||
numInputs={4}
|
|
||||||
inputType="number"
|
|
||||||
inputStyle="inputStyle"
|
|
||||||
placeholder="0000"
|
|
||||||
renderSeparator={<span>-</span>}
|
|
||||||
renderInput={(props) => <input {...props} />}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -71,7 +71,7 @@ export default defineConfig({
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: {
|
||||||
command: "pnpm run start",
|
command: "pnpm run start",
|
||||||
url: process.env.NEXTAUTH_URL,
|
url: process.env.AUTH_URL,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
4162
leaky-ships/pnpm-lock.yaml
generated
4162
leaky-ships/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,8 +1,8 @@
|
||||||
import { useState } from "react"
|
import { createSignal } from "solid-js"
|
||||||
|
|
||||||
function Bluetooth() {
|
function Bluetooth() {
|
||||||
const [startDisabled, setStartDisabled] = useState(true)
|
const [startDisabled, setStartDisabled] = createSignal(true)
|
||||||
const [stopDisabled, setStopDisabled] = useState(true)
|
const [stopDisabled, setStopDisabled] = createSignal(true)
|
||||||
|
|
||||||
const deviceName = "Chromecast Remote"
|
const deviceName = "Chromecast Remote"
|
||||||
// ble UV Index
|
// ble UV Index
|
||||||
|
@ -135,12 +135,12 @@ function Bluetooth() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button id="read" className="bluetooth" onClick={read}>
|
<button id="read" class="bluetooth" onClick={read}>
|
||||||
Connect with BLE device
|
Connect with BLE device
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="start"
|
id="start"
|
||||||
className="bluetooth"
|
class="bluetooth"
|
||||||
disabled={startDisabled}
|
disabled={startDisabled}
|
||||||
onClick={start}
|
onClick={start}
|
||||||
>
|
>
|
||||||
|
@ -148,7 +148,7 @@ function Bluetooth() {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="stop"
|
id="stop"
|
||||||
className="bluetooth"
|
class="bluetooth"
|
||||||
disabled={stopDisabled}
|
disabled={stopDisabled}
|
||||||
onClick={stop}
|
onClick={stop}
|
||||||
>
|
>
|
||||||
|
@ -156,7 +156,7 @@ function Bluetooth() {
|
||||||
</button>
|
</button>
|
||||||
<p>
|
<p>
|
||||||
<span
|
<span
|
||||||
className="App-link"
|
class="App-link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
"chrome://flags/#enable-experimental-web-platform-features",
|
"chrome://flags/#enable-experimental-web-platform-features",
|
||||||
|
@ -169,7 +169,7 @@ function Bluetooth() {
|
||||||
Step 1
|
Step 1
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span
|
<span
|
||||||
className="App-link"
|
class="App-link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
"chrome://flags/#enable-web-bluetooth-new-permissions-backend",
|
"chrome://flags/#enable-web-bluetooth-new-permissions-backend",
|
|
@ -10,14 +10,14 @@ function BurgerMenu({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
id="menu"
|
id="menu"
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"absolute left-4 top-4 flex h-16 w-16 items-center justify-center rounded-lg border-b-2 border-shield-gray bg-grayish shadow-lg duration-100 active:border-b-0 active:border-t-2 md:left-6 md:top-6 md:h-20 md:w-20 md:rounded-xl md:border-b-4 md:active:border-t-4 lg:left-8 lg:top-8 xl:left-12 xl:top-12 xl:h-24 xl:w-24",
|
"absolute left-4 top-4 flex h-16 w-16 items-center justify-center rounded-lg border-b-2 border-shield-gray bg-grayish shadow-lg duration-100 active:border-b-0 active:border-t-2 md:left-6 md:top-6 md:h-20 md:w-20 md:rounded-xl md:border-b-4 md:active:border-t-4 lg:left-8 lg:top-8 xl:left-12 xl:top-12 xl:h-24 xl:w-24",
|
||||||
{ "blur-sm": blur },
|
{ "blur-sm": blur },
|
||||||
)}
|
)}
|
||||||
onClick={() => onClick && setTimeout(onClick, 200)}
|
onClick={() => onClick && setTimeout(onClick, 200)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="pixelart h-12 w-12 md:h-16 md:w-16 xl:h-20 xl:w-20"
|
class="pixelart h-12 w-12 md:h-16 md:w-16 xl:h-20 xl:w-20"
|
||||||
src="/assets/burger-menu.png"
|
src="/assets/burger-menu.png"
|
||||||
alt="Burger Menu"
|
alt="Burger Menu"
|
||||||
/>
|
/>
|
34
leaky-ships/src/components/FontAwesomeIcon.tsx
Normal file
34
leaky-ships/src/components/FontAwesomeIcon.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { JSX } from "solid-js"
|
||||||
|
|
||||||
|
function FontAwesomeIcon({
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
icon: IconDefinition
|
||||||
|
className?: string
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
// focusable="false"
|
||||||
|
data-prefix={icon.prefix}
|
||||||
|
data-icon={icon.iconName}
|
||||||
|
class={classNames(
|
||||||
|
"svg-inline--fa",
|
||||||
|
"fa-".concat(icon.iconName),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox={`0 0 ${icon.icon[0]} ${icon.icon[1]}`}
|
||||||
|
>
|
||||||
|
<path fill="currentColor" d={icon.icon[4] as string}></path>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FontAwesomeIcon
|
|
@ -1,6 +1,6 @@
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
import useIndex from "@hooks/useIndex"
|
import useIndex from "~/hooks/useIndex"
|
||||||
import useShips from "@hooks/useShips"
|
import useShips from "~/hooks/useShips"
|
||||||
import {
|
import {
|
||||||
borderCN,
|
borderCN,
|
||||||
cornerCN,
|
cornerCN,
|
||||||
|
@ -10,8 +10,7 @@ import {
|
||||||
overlapsWithAnyBorder,
|
overlapsWithAnyBorder,
|
||||||
shipProps,
|
shipProps,
|
||||||
targetList,
|
targetList,
|
||||||
} from "@lib/utils/helpers"
|
} from "~/lib/utils/helpers"
|
||||||
import { CSSProperties, useCallback } from "react"
|
|
||||||
import { count } from "./Gamefield"
|
import { count } from "./Gamefield"
|
||||||
|
|
||||||
type TilesType = {
|
type TilesType = {
|
||||||
|
@ -34,15 +33,13 @@ function BorderTiles() {
|
||||||
} = useGameProps()
|
} = useGameProps()
|
||||||
const { ships, setShips, removeShip } = useShips()
|
const { ships, setShips, removeShip } = useShips()
|
||||||
|
|
||||||
const settingTarget = useCallback(
|
const settingTarget = (isGameTile: boolean, x: number, y: number) => {
|
||||||
(isGameTile: boolean, x: number, y: number) => {
|
|
||||||
if (payload?.game?.state === "running") {
|
if (payload?.game?.state === "running") {
|
||||||
const list = targetList(targetPreview, mode)
|
const list = targetList(targetPreview, mode)
|
||||||
if (
|
if (
|
||||||
!isGameTile ||
|
!isGameTile ||
|
||||||
!list.filter(
|
!list.filter(({ x, y }) => !isAlreadyHit(x, y, activeUser?.hits ?? []))
|
||||||
({ x, y }) => !isAlreadyHit(x, y, activeUser?.hits ?? []),
|
.length
|
||||||
).length
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if (!overlapsWithAnyBorder(targetPreview, mode))
|
if (!overlapsWithAnyBorder(targetPreview, mode))
|
||||||
|
@ -55,23 +52,13 @@ function BorderTiles() {
|
||||||
} else if (
|
} else if (
|
||||||
payload?.game?.state === "starting" &&
|
payload?.game?.state === "starting" &&
|
||||||
targetPreview.show &&
|
targetPreview.show &&
|
||||||
!intersectingShip(ships, shipProps(ships, mode, targetPreview)).score
|
!intersectingShip(ships(), shipProps(ships(), mode, targetPreview)).score
|
||||||
) {
|
) {
|
||||||
setMouseCursor((e) => ({ ...e, shouldShow: false }))
|
setMouseCursor((e) => ({ ...e, shouldShow: false }))
|
||||||
setShips([...ships, shipProps(ships, mode, targetPreview)])
|
setShips([...ships(), shipProps(ships(), mode, targetPreview)])
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[
|
|
||||||
activeUser?.hits,
|
|
||||||
mode,
|
|
||||||
payload?.game?.state,
|
|
||||||
setMouseCursor,
|
|
||||||
setShips,
|
|
||||||
setTarget,
|
|
||||||
ships,
|
|
||||||
targetPreview,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
let tilesProperties: TilesType[] = []
|
let tilesProperties: TilesType[] = []
|
||||||
|
|
||||||
for (let y = 0; y < count + 2; y++) {
|
for (let y = 0; y < count + 2; y++) {
|
||||||
|
@ -99,14 +86,13 @@ function BorderTiles() {
|
||||||
{tilesProperties.map(({ key, className, isGameTile, x, y }) => {
|
{tilesProperties.map(({ key, className, isGameTile, x, y }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
class={className}
|
||||||
className={className}
|
style={{ "--x": x, "--y": y }}
|
||||||
style={{ "--x": x, "--y": y } as CSSProperties}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (payload?.game?.state === "running") {
|
if (payload?.game?.state === "running") {
|
||||||
settingTarget(isGameTile, x, y)
|
settingTarget(isGameTile, x, y)
|
||||||
} else if (payload?.game?.state === "starting") {
|
} else if (payload?.game?.state === "starting") {
|
||||||
const { index } = intersectingShip(ships, {
|
const { index } = intersectingShip(ships(), {
|
||||||
...mouseCursor,
|
...mouseCursor,
|
||||||
size: 1,
|
size: 1,
|
||||||
variant: 0,
|
variant: 0,
|
||||||
|
@ -115,7 +101,7 @@ function BorderTiles() {
|
||||||
if (typeof index === "undefined")
|
if (typeof index === "undefined")
|
||||||
settingTarget(isGameTile, x, y)
|
settingTarget(isGameTile, x, y)
|
||||||
else {
|
else {
|
||||||
const ship = ships[index]
|
const ship = ships()[index]
|
||||||
useGameProps.setState({ mode: ship.size - 2 })
|
useGameProps.setState({ mode: ship.size - 2 })
|
||||||
removeShip(ship)
|
removeShip(ship)
|
||||||
setMouseCursor((e) => ({ ...e, shouldShow: true }))
|
setMouseCursor((e) => ({ ...e, shouldShow: true }))
|
||||||
|
@ -130,8 +116,8 @@ function BorderTiles() {
|
||||||
isGameTile &&
|
isGameTile &&
|
||||||
(payload?.game?.state === "starting"
|
(payload?.game?.state === "starting"
|
||||||
? intersectingShip(
|
? intersectingShip(
|
||||||
ships,
|
ships(),
|
||||||
shipProps(ships, mode, {
|
shipProps(ships(), mode, {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
orientation: targetPreview.orientation,
|
orientation: targetPreview.orientation,
|
373
leaky-ships/src/components/Gamefield/EventBar.tsx
Normal file
373
leaky-ships/src/components/Gamefield/EventBar.tsx
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
import {
|
||||||
|
faSquare2,
|
||||||
|
faSquare3,
|
||||||
|
faSquare4,
|
||||||
|
} from "@fortawesome/pro-regular-svg-icons"
|
||||||
|
import {
|
||||||
|
faBroomWide,
|
||||||
|
faCheck,
|
||||||
|
faComments,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faFlag,
|
||||||
|
faGlasses,
|
||||||
|
faLock,
|
||||||
|
faPalette,
|
||||||
|
faReply,
|
||||||
|
faRotate,
|
||||||
|
faScribble,
|
||||||
|
faShip,
|
||||||
|
faSparkles,
|
||||||
|
faSwords,
|
||||||
|
faXmark,
|
||||||
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { socket } from "~/lib/socket"
|
||||||
|
import { modes } from "~/lib/utils/helpers"
|
||||||
|
import { GamePropsSchema } from "~/lib/zodSchemas"
|
||||||
|
// import { Icons, toast } from "react-toastify"
|
||||||
|
import { createEffect, createMemo } from "solid-js"
|
||||||
|
import { useNavigate } from "solid-start"
|
||||||
|
import { useDrawProps } from "~/hooks/useDrawProps"
|
||||||
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
|
import useIndex from "~/hooks/useIndex"
|
||||||
|
import useShips from "~/hooks/useShips"
|
||||||
|
import { EventBarModes, GameSettings } from "../../interfaces/frontend"
|
||||||
|
import Item from "./Item"
|
||||||
|
|
||||||
|
export function setGameSetting(
|
||||||
|
payload: GameSettings,
|
||||||
|
setSetting: (settings: GameSettings) => string | null,
|
||||||
|
full: (payload: GamePropsSchema) => void,
|
||||||
|
) {
|
||||||
|
return () => {
|
||||||
|
const hash = setSetting(payload)
|
||||||
|
socket.emit("gameSetting", payload, (newHash) => {
|
||||||
|
if (newHash === hash) return
|
||||||
|
console.log("hash", hash, newHash)
|
||||||
|
socket.emit("update", full)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventBar({ clear }: { clear: () => void }) {
|
||||||
|
const { shouldHide, color } = useDrawProps()
|
||||||
|
const { selfIndex, isActiveIndex, selfUser } = useIndex()
|
||||||
|
const { ships } = useShips()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const {
|
||||||
|
payload,
|
||||||
|
userStates,
|
||||||
|
menu,
|
||||||
|
mode,
|
||||||
|
setSetting,
|
||||||
|
full,
|
||||||
|
target,
|
||||||
|
setTarget,
|
||||||
|
setTargetPreview,
|
||||||
|
setIsReady,
|
||||||
|
reset,
|
||||||
|
} = useGameProps()
|
||||||
|
const gameSetting = (payload: GameSettings) =>
|
||||||
|
setGameSetting(payload, setSetting, full)
|
||||||
|
|
||||||
|
const items = createMemo<EventBarModes>(() => ({
|
||||||
|
main: [
|
||||||
|
{
|
||||||
|
icon: "burger-menu",
|
||||||
|
text: "Menu",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "menu" })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
payload?.game?.state === "running"
|
||||||
|
? {
|
||||||
|
icon: faSwords,
|
||||||
|
text: "Attack",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "moves" })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
icon: faShip,
|
||||||
|
text: "Ships",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "moves" })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "pen",
|
||||||
|
text: "Draw",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "draw" })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "gear",
|
||||||
|
text: "Settings",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "settings" })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
menu: [
|
||||||
|
{
|
||||||
|
icon: faFlag,
|
||||||
|
text: "Surrender",
|
||||||
|
iconColor: "darkred",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "surrender" })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
moves:
|
||||||
|
payload?.game?.state === "running"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: "scope",
|
||||||
|
text: "Fire missile",
|
||||||
|
enabled: mode === 0,
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ mode: 0 })
|
||||||
|
setTarget((e) => ({ ...e, show: false }))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "torpedo",
|
||||||
|
text: "Fire torpedo",
|
||||||
|
enabled: mode === 1 || mode === 2,
|
||||||
|
amount:
|
||||||
|
2 -
|
||||||
|
((selfUser?.moves ?? []).filter(
|
||||||
|
(e) => e.type === "htorpedo" || e.type === "vtorpedo",
|
||||||
|
).length ?? 0),
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ mode: 1 })
|
||||||
|
setTarget((e) => ({ ...e, show: false }))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "radar",
|
||||||
|
text: "Radar scan",
|
||||||
|
enabled: mode === 3,
|
||||||
|
amount:
|
||||||
|
1 -
|
||||||
|
((selfUser?.moves ?? []).filter((e) => e.type === "radar")
|
||||||
|
.length ?? 0),
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ mode: 3 })
|
||||||
|
setTarget((e) => ({ ...e, show: false }))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
icon: faSquare2,
|
||||||
|
text: "Minensucher",
|
||||||
|
amount: 1 - ships().filter((e) => e.size === 2).length,
|
||||||
|
callback: () => {
|
||||||
|
if (1 - ships().filter((e) => e.size === 2).length === 0) return
|
||||||
|
useGameProps.setState({ mode: 0 })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faSquare3,
|
||||||
|
text: "Kreuzer",
|
||||||
|
amount: 3 - ships().filter((e) => e.size === 3).length,
|
||||||
|
callback: () => {
|
||||||
|
if (3 - ships().filter((e) => e.size === 3).length === 0) return
|
||||||
|
useGameProps.setState({ mode: 1 })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faSquare4,
|
||||||
|
text: "Schlachtschiff",
|
||||||
|
amount: 2 - ships().filter((e) => e.size === 4).length,
|
||||||
|
callback: () => {
|
||||||
|
if (2 - ships().filter((e) => e.size === 4).length === 0) return
|
||||||
|
useGameProps.setState({ mode: 2 })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faRotate,
|
||||||
|
text: "Rotate",
|
||||||
|
callback: () => {
|
||||||
|
setTargetPreview((t) => ({
|
||||||
|
...t,
|
||||||
|
orientation: t.orientation === "h" ? "v" : "h",
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
draw: [
|
||||||
|
{ icon: faBroomWide, text: "Clear", callback: clear },
|
||||||
|
{ icon: faPalette, text: "Color", iconColor: color },
|
||||||
|
{
|
||||||
|
icon: shouldHide ? faEye : faEyeSlash,
|
||||||
|
text: shouldHide ? "Show" : "Hide",
|
||||||
|
callback: () => {
|
||||||
|
useDrawProps.setState({ shouldHide: !shouldHide })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
icon: faGlasses,
|
||||||
|
text: "Spectators",
|
||||||
|
disabled: !payload?.game?.allowSpectators,
|
||||||
|
callback: gameSetting({
|
||||||
|
allowSpectators: !payload?.game?.allowSpectators,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faSparkles,
|
||||||
|
text: "Specials",
|
||||||
|
disabled: !payload?.game?.allowSpecials,
|
||||||
|
callback: gameSetting({
|
||||||
|
allowSpecials: !payload?.game?.allowSpecials,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faComments,
|
||||||
|
text: "Chat",
|
||||||
|
disabled: !payload?.game?.allowChat,
|
||||||
|
callback: gameSetting({ allowChat: !payload?.game?.allowChat }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faScribble,
|
||||||
|
text: "Mark/Draw",
|
||||||
|
disabled: !payload?.game?.allowMarkDraw,
|
||||||
|
callback: gameSetting({
|
||||||
|
allowMarkDraw: !payload?.game?.allowMarkDraw,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
surrender: [
|
||||||
|
{
|
||||||
|
icon: faCheck,
|
||||||
|
text: "Yes",
|
||||||
|
iconColor: "green",
|
||||||
|
callback: async () => {
|
||||||
|
socket.emit("gameState", "aborted")
|
||||||
|
await navigate("/")
|
||||||
|
reset()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faXmark,
|
||||||
|
text: "No",
|
||||||
|
iconColor: "red",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "main" })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (
|
||||||
|
menu !== "moves" ||
|
||||||
|
payload?.game?.state !== "starting" ||
|
||||||
|
mode < 0 ||
|
||||||
|
items().moves[mode].amount
|
||||||
|
)
|
||||||
|
return
|
||||||
|
const index = items().moves.findIndex((e) => e.amount)
|
||||||
|
useGameProps.setState({ mode: index })
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
useDrawProps.setState({ enable: menu === "draw" })
|
||||||
|
}, [menu])
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (payload?.game?.state !== "running") return
|
||||||
|
|
||||||
|
let toastId = "otherPlayer"
|
||||||
|
// if (isActiveIndex) toast.dismiss(toastId)
|
||||||
|
// else
|
||||||
|
// toast.info("Waiting for other player...", {
|
||||||
|
// toastId,
|
||||||
|
// position: "top-right",
|
||||||
|
// icon: Icons.spinner(),
|
||||||
|
// autoClose: false,
|
||||||
|
// hideProgressBar: true,
|
||||||
|
// closeButton: false,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// toastId = "connect_error"
|
||||||
|
// const isActive = toast.isActive(toastId)
|
||||||
|
// console.log(toastId, isActive)
|
||||||
|
// if (isActive)
|
||||||
|
// toast.update(toastId, {
|
||||||
|
// autoClose: 5000,
|
||||||
|
// })
|
||||||
|
// else
|
||||||
|
// toast.warn("Spie", { toastId })
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="event-bar">
|
||||||
|
{menu !== "main" ? (
|
||||||
|
<Item
|
||||||
|
props={{
|
||||||
|
icon: faReply,
|
||||||
|
text: "Return",
|
||||||
|
iconColor: "#555",
|
||||||
|
callback: () => {
|
||||||
|
useGameProps.setState({ menu: "main" })
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{items()[menu].map((e, i) => {
|
||||||
|
if (!isActiveIndex && menu === "main" && i === 1) return
|
||||||
|
return <Item props={e} />
|
||||||
|
})}
|
||||||
|
{menu === "moves" ? (
|
||||||
|
<Item
|
||||||
|
props={{
|
||||||
|
icon:
|
||||||
|
selfIndex >= 0 && userStates[selfIndex].isReady
|
||||||
|
? faLock
|
||||||
|
: faCheck,
|
||||||
|
text:
|
||||||
|
selfIndex >= 0 && userStates[selfIndex].isReady
|
||||||
|
? "unready"
|
||||||
|
: "Done",
|
||||||
|
disabled:
|
||||||
|
payload?.game?.state === "starting" ? mode >= 0 : undefined,
|
||||||
|
enabled:
|
||||||
|
payload?.game?.state === "running" && mode >= 0 && target.show,
|
||||||
|
callback: () => {
|
||||||
|
if (selfIndex < 0) return
|
||||||
|
switch (payload?.game?.state) {
|
||||||
|
case "starting":
|
||||||
|
const isReady = !userStates[selfIndex].isReady
|
||||||
|
setIsReady({ isReady, i: selfIndex })
|
||||||
|
socket.emit("isReady", isReady)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "running":
|
||||||
|
const i = (selfUser?.moves ?? [])
|
||||||
|
.map((e) => e.index)
|
||||||
|
.reduce((prev, curr) => (curr > prev ? curr : prev), 0)
|
||||||
|
const props = {
|
||||||
|
type: modes[mode].type,
|
||||||
|
x: target.x,
|
||||||
|
y: target.y,
|
||||||
|
orientation: target.orientation,
|
||||||
|
index: (selfUser?.moves ?? []).length ? i + 1 : 0,
|
||||||
|
}
|
||||||
|
socket.emit("dispatchMove", props)
|
||||||
|
setTarget((t) => ({ ...t, show: false }))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventBar
|
13
leaky-ships/src/components/Gamefield/FogImages.tsx
Normal file
13
leaky-ships/src/components/Gamefield/FogImages.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
function FogImages() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img class="fog left" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
||||||
|
<img class="fog right" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
||||||
|
<img class="fog top" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
||||||
|
<img class="fog bottom" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
||||||
|
<img class="fog middle" src={`/fog/fog4.png`} alt={`fog4.png`} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FogImages
|
|
@ -1,19 +1,19 @@
|
||||||
// import Bluetooth from "./Bluetooth"
|
// import Bluetooth from "./Bluetooth"
|
||||||
// import FogImages from "./FogImages"
|
// import FogImages from "./FogImages"
|
||||||
import BorderTiles from "@components/Gamefield/BorderTiles"
|
// import { toast } from "react-toastify"
|
||||||
import EventBar from "@components/Gamefield/EventBar"
|
import { createEffect } from "solid-js"
|
||||||
import HitElems from "@components/Gamefield/HitElems"
|
import { useNavigate } from "solid-start"
|
||||||
import Targets from "@components/Gamefield/Targets"
|
import BorderTiles from "~/components/Gamefield/BorderTiles"
|
||||||
import { useDraw } from "@hooks/useDraw"
|
import EventBar from "~/components/Gamefield/EventBar"
|
||||||
import { useDrawProps } from "@hooks/useDrawProps"
|
import HitElems from "~/components/Gamefield/HitElems"
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import Targets from "~/components/Gamefield/Targets"
|
||||||
import useIndex from "@hooks/useIndex"
|
import { useDraw } from "~/hooks/useDraw"
|
||||||
import useSocket from "@hooks/useSocket"
|
import { useDrawProps } from "~/hooks/useDrawProps"
|
||||||
import { socket } from "@lib/socket"
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
import { overlapsWithAnyBorder } from "@lib/utils/helpers"
|
import useIndex from "~/hooks/useIndex"
|
||||||
import { useRouter } from "next/router"
|
import useSocket from "~/hooks/useSocket"
|
||||||
import { CSSProperties, useEffect } from "react"
|
import { socket } from "~/lib/socket"
|
||||||
import { toast } from "react-toastify"
|
import { overlapsWithAnyBorder } from "~/lib/utils/helpers"
|
||||||
import Labeling from "./Labeling"
|
import Labeling from "./Labeling"
|
||||||
import Ships from "./Ships"
|
import Ships from "./Ships"
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export const count = 12
|
||||||
|
|
||||||
function Gamefield() {
|
function Gamefield() {
|
||||||
const { selfUser } = useIndex()
|
const { selfUser } = useIndex()
|
||||||
const router = useRouter()
|
const navigate = useNavigate()
|
||||||
const {
|
const {
|
||||||
userStates,
|
userStates,
|
||||||
mode,
|
mode,
|
||||||
|
@ -38,7 +38,7 @@ function Gamefield() {
|
||||||
const { canvasRef, onMouseDown, clear } = useDraw()
|
const { canvasRef, onMouseDown, clear } = useDraw()
|
||||||
const { enable, color, shouldHide } = useDrawProps()
|
const { enable, color, shouldHide } = useDrawProps()
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
if (
|
if (
|
||||||
payload?.game?.state !== "starting" ||
|
payload?.game?.state !== "starting" ||
|
||||||
userStates.reduce((prev, curr) => prev || !curr.isReady, false)
|
userStates.reduce((prev, curr) => prev || !curr.isReady, false)
|
||||||
|
@ -48,12 +48,12 @@ function Gamefield() {
|
||||||
socket.emit("gameState", "running")
|
socket.emit("gameState", "running")
|
||||||
}, [payload?.game?.state, selfUser?.ships, userStates])
|
}, [payload?.game?.state, selfUser?.ships, userStates])
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
if (payload?.game?.id || !isConnected) return
|
if (payload?.game?.id || !isConnected) return
|
||||||
socket.emit("update", full)
|
socket.emit("update", full)
|
||||||
}, [full, payload?.game?.id, isConnected])
|
}, [full, payload?.game?.id, isConnected])
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
if (mode < 0) return
|
if (mode < 0) return
|
||||||
const { x, y, show } = target
|
const { x, y, show } = target
|
||||||
const { shouldShow, ...position } = mouseCursor
|
const { shouldShow, ...position } = mouseCursor
|
||||||
|
@ -87,25 +87,25 @@ function Gamefield() {
|
||||||
}
|
}
|
||||||
}, [mode, mouseCursor, payload?.game?.state, setTargetPreview, target])
|
}, [mode, mouseCursor, payload?.game?.state, setTargetPreview, target])
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
if (payload?.game?.state !== "aborted") return
|
if (payload?.game?.state !== "aborted") return
|
||||||
toast.info("Enemy gave up!")
|
// toast.info("Enemy gave up!")
|
||||||
router.push("/")
|
navigate("/")
|
||||||
reset()
|
reset()
|
||||||
}, [payload?.game?.state, reset, router])
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
if (payload?.game?.id) return
|
if (payload?.game?.id) return
|
||||||
const timeout = setTimeout(() => router.push("/"), 5000)
|
const timeout = setTimeout(() => navigate("/"), 5000)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}, [payload?.game?.id, router])
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="gamefield">
|
<div id="gamefield">
|
||||||
{/* <Bluetooth /> */}
|
{/* <Bluetooth /> */}
|
||||||
<div
|
<div
|
||||||
id="game-frame"
|
id="game-frame"
|
||||||
style={{ "--i": count } as CSSProperties}
|
style={{ "--i": count }}
|
||||||
onMouseLeave={() =>
|
onMouseLeave={() =>
|
||||||
setMouseCursor((e) => ({ ...e, shouldShow: false }))
|
setMouseCursor((e) => ({ ...e, shouldShow: false }))
|
||||||
}
|
}
|
||||||
|
@ -124,13 +124,11 @@ function Gamefield() {
|
||||||
<Targets />
|
<Targets />
|
||||||
|
|
||||||
<canvas
|
<canvas
|
||||||
style={
|
style={{
|
||||||
{
|
|
||||||
opacity: shouldHide ? 0 : 1,
|
opacity: shouldHide ? 0 : 1,
|
||||||
boxShadow: enable ? "inset 0 0 0 2px " + color : "none",
|
"box-shadow": enable ? "inset 0 0 0 2px " + color : "none",
|
||||||
pointerEvents: enable && !shouldHide ? "auto" : "none",
|
"pointer-events": enable && !shouldHide ? "auto" : "none",
|
||||||
} as CSSProperties
|
}}
|
||||||
}
|
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
width="648"
|
width="648"
|
|
@ -1,8 +1,8 @@
|
||||||
import { faCrosshairs } from "@fortawesome/pro-solid-svg-icons"
|
import { faCrosshairs } from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { faRadar } from "@fortawesome/pro-thin-svg-icons"
|
import { faRadar } from "@fortawesome/pro-thin-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties } from "react"
|
import {} from "solid-js"
|
||||||
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
import { PointerProps } from "../../interfaces/frontend"
|
import { PointerProps } from "../../interfaces/frontend"
|
||||||
|
|
||||||
function GamefieldPointer({
|
function GamefieldPointer({
|
||||||
|
@ -18,12 +18,12 @@ function GamefieldPointer({
|
||||||
: { "--x1": x - 1, "--x2": x + 2, "--y1": y - 1, "--y2": y + 2 }
|
: { "--x1": x - 1, "--x2": x + 2, "--y1": y - 1, "--y2": y + 2 }
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames("hit-svg", "target", type, ...edges, {
|
class={classNames("hit-svg", "target", type, ...edges, {
|
||||||
preview: preview,
|
preview: preview,
|
||||||
show: show,
|
show: show,
|
||||||
imply: imply,
|
imply: imply,
|
||||||
})}
|
})}
|
||||||
style={style as CSSProperties}
|
style={style}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={!isRadar ? faCrosshairs : faRadar} />
|
<FontAwesomeIcon icon={!isRadar ? faCrosshairs : faRadar} />
|
||||||
</div>
|
</div>
|
|
@ -1,7 +1,6 @@
|
||||||
import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons"
|
import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
import useIndex from "@hooks/useIndex"
|
import useIndex from "~/hooks/useIndex"
|
||||||
import { CSSProperties } from "react"
|
|
||||||
import { Hit } from "../../interfaces/frontend"
|
import { Hit } from "../../interfaces/frontend"
|
||||||
|
|
||||||
function HitElems({
|
function HitElems({
|
||||||
|
@ -14,18 +13,12 @@ function HitElems({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(props?.hits ?? activeUser?.hits ?? []).map(({ hit, x, y }, i) => (
|
{(props?.hits ?? activeUser?.hits ?? []).map(({ hit, x, y }, i) => (
|
||||||
<div
|
<div class="hit-svg" style={{ "--x": x, "--y": y }}>
|
||||||
key={i}
|
|
||||||
className="hit-svg"
|
|
||||||
style={{ "--x": x, "--y": y } as CSSProperties}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={hit ? faBurst : faXmark}
|
icon={hit ? faBurst : faXmark}
|
||||||
style={
|
style={{
|
||||||
{
|
|
||||||
color: props?.colorOverride || (hit ? "red" : undefined),
|
color: props?.colorOverride || (hit ? "red" : undefined),
|
||||||
} as CSSProperties
|
}}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
|
@ -1,8 +1,8 @@
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { createEffect, createSignal } from "solid-js"
|
||||||
import { useDrawProps } from "@hooks/useDrawProps"
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
|
import { useDrawProps } from "~/hooks/useDrawProps"
|
||||||
|
// import { HexColorPicker } from "react-colorful"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties, useEffect, useRef, useState } from "react"
|
|
||||||
import { HexColorPicker } from "react-colorful"
|
|
||||||
import { ItemProps } from "../../interfaces/frontend"
|
import { ItemProps } from "../../interfaces/frontend"
|
||||||
|
|
||||||
function Item({
|
function Item({
|
||||||
|
@ -12,13 +12,12 @@ function Item({
|
||||||
}) {
|
}) {
|
||||||
const isColor = text === "Color"
|
const isColor = text === "Color"
|
||||||
const { color, setColor } = useDrawProps()
|
const { color, setColor } = useDrawProps()
|
||||||
const [active, setActive] = useState(false)
|
const [active, setActive] = createSignal(false)
|
||||||
const cpRef = useRef<HTMLDivElement>(null)
|
let cpRef: HTMLDivElement
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const inActive = (e: MouseEvent) => {
|
const inActive = (e: MouseEvent) => {
|
||||||
if (cpRef.current && !cpRef.current.contains(e.target as Node))
|
if (cpRef && !cpRef.contains(e.target as Node)) setActive(false)
|
||||||
setActive(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
|
@ -32,26 +31,26 @@ function Item({
|
||||||
}, [active, isColor])
|
}, [active, isColor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="item" onClick={isColor ? () => setActive(true) : callback}>
|
<div class="item" onClick={isColor ? () => setActive(true) : callback}>
|
||||||
{isColor ? (
|
{isColor ? (
|
||||||
<div
|
<div
|
||||||
ref={cpRef}
|
ref={cpRef!}
|
||||||
className={classNames("react-colorful-wrapper", { active: active })}
|
class={classNames("react-colorful-wrapper", { active: active })}
|
||||||
>
|
>
|
||||||
<HexColorPicker color={color} onChange={setColor} />
|
{/* <HexColorPicker color={color} onChange={setColor} /> */}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={classNames("container", {
|
class={classNames("container", {
|
||||||
amount: typeof amount !== "undefined",
|
amount: typeof amount !== "undefined",
|
||||||
disabled: disabled || amount === 0,
|
disabled: disabled || amount === 0,
|
||||||
enabled: disabled === false || enabled,
|
enabled: disabled === false || enabled,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
typeof amount !== "undefined"
|
typeof amount !== "undefined"
|
||||||
? ({
|
? {
|
||||||
"--amount": JSON.stringify(amount.toString()),
|
"--amount": JSON.stringify(amount.toString()),
|
||||||
} as CSSProperties)
|
}
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -59,10 +58,13 @@ function Item({
|
||||||
<img
|
<img
|
||||||
src={`/assets/${icon}.png`}
|
src={`/assets/${icon}.png`}
|
||||||
alt={`${icon}.png`}
|
alt={`${icon}.png`}
|
||||||
className="pixelart"
|
class="pixelart"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon icon={icon} color={iconColor ?? "#444"} />
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
// color={iconColor ?? "#444"}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
|
@ -1,6 +1,6 @@
|
||||||
import { fieldIndex } from "@lib/utils/helpers"
|
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties } from "react"
|
import {} from "solid-js"
|
||||||
|
import { fieldIndex } from "~/lib/utils/helpers"
|
||||||
import { Field } from "../../interfaces/frontend"
|
import { Field } from "../../interfaces/frontend"
|
||||||
import { count } from "./Gamefield"
|
import { count } from "./Gamefield"
|
||||||
|
|
||||||
|
@ -37,9 +37,8 @@ function Labeling() {
|
||||||
<>
|
<>
|
||||||
{elems.map(({ field, x, y, orientation }, i) => (
|
{elems.map(({ field, x, y, orientation }, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
class={classNames("label", orientation, field)}
|
||||||
className={classNames("label", orientation, field)}
|
style={{ "--x": x, "--y": y }}
|
||||||
style={{ "--x": x, "--y": y } as CSSProperties}
|
|
||||||
>
|
>
|
||||||
{field}
|
{field}
|
||||||
</span>
|
</span>
|
|
@ -1,5 +1,5 @@
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties, useEffect, useRef } from "react"
|
import { createEffect } from "solid-js"
|
||||||
import { ShipProps } from "../../interfaces/frontend"
|
import { ShipProps } from "../../interfaces/frontend"
|
||||||
|
|
||||||
const sizes: { [n: number]: number } = {
|
const sizes: { [n: number]: number } = {
|
||||||
|
@ -20,10 +20,10 @@ function Ship({
|
||||||
color?: string
|
color?: string
|
||||||
}) {
|
}) {
|
||||||
const filename = `ship_blue_${size}x_${variant}.gif`
|
const filename = `ship_blue_${size}x_${variant}.gif`
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
let canvasRef: HTMLCanvasElement
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef
|
||||||
const ctx = canvas?.getContext("2d")
|
const ctx = canvas?.getContext("2d")
|
||||||
if (!canvas || !ctx) return
|
if (!canvas || !ctx) return
|
||||||
const gif = new Image()
|
const gif = new Image()
|
||||||
|
@ -46,15 +46,13 @@ function Ship({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames("ship", "s" + size, orientation, {
|
class={classNames("ship", "s" + size, orientation, {
|
||||||
preview: preview,
|
preview: preview,
|
||||||
warn: warn,
|
warn: warn,
|
||||||
})}
|
})}
|
||||||
style={
|
style={{ "--x": x, "--y": y, "--color": color ?? "limegreen" }}
|
||||||
{ "--x": x, "--y": y, "--color": color ?? "limegreen" } as CSSProperties
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef!} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
import useIndex from "@hooks/useIndex"
|
import useIndex from "~/hooks/useIndex"
|
||||||
import Ship from "./Ship"
|
import Ship from "./Ship"
|
||||||
|
|
||||||
function Ships() {
|
function Ships() {
|
||||||
|
@ -8,9 +8,7 @@ function Ships() {
|
||||||
|
|
||||||
if (payload?.game?.state === "running" && isActiveIndex) return null
|
if (payload?.game?.state === "running" && isActiveIndex) return null
|
||||||
|
|
||||||
return (
|
return <>{selfUser?.ships.map((props, i) => <Ship props={props} />)}</>
|
||||||
<>{selfUser?.ships.map((props, i) => <Ship key={i} props={props} />)}</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Ships
|
export default Ships
|
|
@ -1,11 +1,11 @@
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
import useIndex from "@hooks/useIndex"
|
import useIndex from "~/hooks/useIndex"
|
||||||
import useShips from "@hooks/useShips"
|
import useShips from "~/hooks/useShips"
|
||||||
import {
|
import {
|
||||||
composeTargetTiles,
|
composeTargetTiles,
|
||||||
intersectingShip,
|
intersectingShip,
|
||||||
shipProps,
|
shipProps,
|
||||||
} from "@lib/utils/helpers"
|
} from "~/lib/utils/helpers"
|
||||||
import GamefieldPointer from "./GamefieldPointer"
|
import GamefieldPointer from "./GamefieldPointer"
|
||||||
import HitElems from "./HitElems"
|
import HitElems from "./HitElems"
|
||||||
import Ship from "./Ship"
|
import Ship from "./Ship"
|
||||||
|
@ -21,23 +21,21 @@ function Targets() {
|
||||||
<>
|
<>
|
||||||
{[
|
{[
|
||||||
...composeTargetTiles(target, mode, activeUser?.hits ?? []).map(
|
...composeTargetTiles(target, mode, activeUser?.hits ?? []).map(
|
||||||
(props, i) => <GamefieldPointer key={"t" + i} props={props} />,
|
(props, i) => <GamefieldPointer props={props} />,
|
||||||
),
|
),
|
||||||
...composeTargetTiles(
|
...composeTargetTiles(
|
||||||
targetPreview,
|
targetPreview,
|
||||||
mode,
|
mode,
|
||||||
activeUser?.hits ?? [],
|
activeUser?.hits ?? [],
|
||||||
).map((props, i) => (
|
).map((props, i) => <GamefieldPointer props={props} preview />),
|
||||||
<GamefieldPointer key={"p" + i} props={props} preview />
|
|
||||||
)),
|
|
||||||
]}
|
]}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
case "starting":
|
case "starting":
|
||||||
if (mode < 0 && !targetPreview.show) return null
|
if (mode < 0 && !targetPreview.show) return null
|
||||||
const ship = shipProps(ships, mode, targetPreview)
|
const ship = shipProps(ships(), mode, targetPreview)
|
||||||
const { fields, borders, score } = intersectingShip(ships, ship)
|
const { fields, borders, score } = intersectingShip(ships(), ship)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Ship
|
<Ship
|
||||||
|
@ -46,7 +44,6 @@ function Targets() {
|
||||||
color={
|
color={
|
||||||
fields.length ? "red" : borders.length ? "orange" : undefined
|
fields.length ? "red" : borders.length ? "orange" : undefined
|
||||||
}
|
}
|
||||||
key={targetPreview.orientation}
|
|
||||||
props={ship}
|
props={ship}
|
||||||
/>
|
/>
|
||||||
<HitElems
|
<HitElems
|
|
@ -1,23 +1,23 @@
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties, useEffect, useMemo, useState } from "react"
|
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||||
|
|
||||||
function Grid() {
|
function Grid() {
|
||||||
function floorClient(number: number) {
|
function floorClient(number: number) {
|
||||||
return Math.floor(number / 50)
|
return Math.floor(number / 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [columns, setColumns] = useState(0)
|
const [columns, setColumns] = createSignal(0)
|
||||||
const [rows, setRows] = useState(0)
|
const [rows, setRows] = createSignal(0)
|
||||||
const [params, setParams] = useState({
|
const [params, setParams] = createSignal({
|
||||||
columns,
|
columns: columns(),
|
||||||
rows,
|
rows: rows(),
|
||||||
quantity: columns * rows,
|
quantity: columns() * rows(),
|
||||||
})
|
})
|
||||||
const [position, setPosition] = useState([0, 0])
|
const [position, setPosition] = createSignal([0, 0])
|
||||||
const [active, setActve] = useState(false)
|
const [active, setActve] = createSignal(false)
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = createSignal(0)
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
setColumns(floorClient(document.body.clientWidth))
|
setColumns(floorClient(document.body.clientWidth))
|
||||||
setRows(floorClient(document.body.clientHeight))
|
setRows(floorClient(document.body.clientHeight))
|
||||||
|
@ -26,14 +26,18 @@ function Grid() {
|
||||||
window.addEventListener("resize", handleResize)
|
window.addEventListener("resize", handleResize)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setParams({ columns, rows, quantity: columns * rows })
|
setParams({
|
||||||
|
columns: columns(),
|
||||||
|
rows: rows(),
|
||||||
|
quantity: columns() * rows(),
|
||||||
|
})
|
||||||
}, 500)
|
}, 500)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}, [columns, rows])
|
})
|
||||||
|
|
||||||
const createTiles = useMemo(() => {
|
const createTiles = createMemo(() => {
|
||||||
const colors = [
|
const colors = [
|
||||||
"rgb(229, 57, 53)",
|
"rgb(229, 57, 53)",
|
||||||
"rgb(253, 216, 53)",
|
"rgb(253, 216, 53)",
|
||||||
|
@ -44,14 +48,14 @@ function Grid() {
|
||||||
]
|
]
|
||||||
|
|
||||||
function createTile(index: number) {
|
function createTile(index: number) {
|
||||||
const x = index % params.columns
|
const x = index % params().columns
|
||||||
const y = Math.floor(index / params.columns)
|
const y = Math.floor(index / params().columns)
|
||||||
const xDiff = (x - position[0]) / 20
|
const xDiff = (x - position()[0]) / 20
|
||||||
const yDiff = (y - position[1]) / 20
|
const yDiff = (y - position()[1]) / 20
|
||||||
const pos = Math.sqrt(xDiff * xDiff + yDiff * yDiff).toFixed(2)
|
const pos = Math.sqrt(xDiff * xDiff + yDiff * yDiff).toFixed(2)
|
||||||
|
|
||||||
function doEffect(posX: number, posY: number) {
|
function doEffect(posX: number, posY: number) {
|
||||||
if (active) return
|
if (active()) return
|
||||||
setPosition([posX, posY])
|
setPosition([posX, posY])
|
||||||
setActve(true)
|
setActve(true)
|
||||||
|
|
||||||
|
@ -66,9 +70,9 @@ function Grid() {
|
||||||
}
|
}
|
||||||
const diagonals = [
|
const diagonals = [
|
||||||
pos(0, 0),
|
pos(0, 0),
|
||||||
pos(params.columns, 0),
|
pos(params().columns, 0),
|
||||||
pos(0, params.rows),
|
pos(0, params().rows),
|
||||||
pos(params.columns, params.rows),
|
pos(params().columns, params().rows),
|
||||||
]
|
]
|
||||||
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
|
@ -82,9 +86,8 @@ function Grid() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
class={classNames({ tile: true, active: active() })}
|
||||||
className={classNames("tile", { active: active })}
|
style={{ "--delay": pos + "s" }}
|
||||||
style={{ "--delay": pos + "s" } as CSSProperties}
|
|
||||||
onClick={() => doEffect(x, y)}
|
onClick={() => doEffect(x, y)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -93,23 +96,21 @@ function Grid() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="tiles"
|
id="tiles"
|
||||||
style={
|
style={{
|
||||||
{
|
"--columns": params().columns,
|
||||||
"--columns": params.columns,
|
"--rows": params().rows,
|
||||||
"--rows": params.rows,
|
"--bg-color-1": colors[count() % colors.length],
|
||||||
"--bg-color-1": colors[count % colors.length],
|
"--bg-color-2": colors[(count() + 1) % colors.length],
|
||||||
"--bg-color-2": colors[(count + 1) % colors.length],
|
}}
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{Array.from(Array(params.quantity), (_tile, index) =>
|
{Array.from(Array(params().quantity), (_tile, index) =>
|
||||||
createTile(index),
|
createTile(index),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [params, position, active, count])
|
}, [params, position, active, count])
|
||||||
|
|
||||||
return createTiles
|
return createTiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Grid
|
export default Grid
|
|
@ -1,24 +1,24 @@
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties, useEffect, useMemo, useState } from "react"
|
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||||
|
|
||||||
function Grid2() {
|
function Grid2() {
|
||||||
function floorClient(number: number) {
|
function floorClient(number: number) {
|
||||||
return Math.floor(number / 50)
|
return Math.floor(number / 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [columns, setColumns] = useState(0)
|
const [columns, setColumns] = createSignal(0)
|
||||||
const [rows, setRows] = useState(0)
|
const [rows, setRows] = createSignal(0)
|
||||||
const [params, setParams] = useState({
|
const [params, setParams] = createSignal({
|
||||||
columns,
|
columns: columns(),
|
||||||
rows,
|
rows: rows(),
|
||||||
quantity: columns * rows,
|
quantity: columns() * rows(),
|
||||||
})
|
})
|
||||||
const [position, setPosition] = useState([0, 0])
|
const [position, setPosition] = createSignal([0, 0])
|
||||||
const [active, setActve] = useState(false)
|
const [active, setActve] = createSignal(false)
|
||||||
const [action, setAction] = useState(false)
|
const [action, setAction] = createSignal(false)
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = createSignal(0)
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
setColumns(floorClient(document.body.clientWidth))
|
setColumns(floorClient(document.body.clientWidth))
|
||||||
setRows(floorClient(document.body.clientHeight))
|
setRows(floorClient(document.body.clientHeight))
|
||||||
|
@ -27,14 +27,18 @@ function Grid2() {
|
||||||
window.addEventListener("resize", handleResize)
|
window.addEventListener("resize", handleResize)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setParams({ columns, rows, quantity: columns * rows })
|
setParams({
|
||||||
|
columns: columns(),
|
||||||
|
rows: rows(),
|
||||||
|
quantity: columns() * rows(),
|
||||||
|
})
|
||||||
}, 500)
|
}, 500)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}, [columns, rows])
|
}, [columns, rows])
|
||||||
|
|
||||||
const createTiles = useMemo(() => {
|
const createTiles = createMemo(() => {
|
||||||
const sentences = [
|
const sentences = [
|
||||||
"Ethem ...",
|
"Ethem ...",
|
||||||
"hat ...",
|
"hat ...",
|
||||||
|
@ -44,14 +48,14 @@ function Grid2() {
|
||||||
]
|
]
|
||||||
|
|
||||||
function createTile(index: number) {
|
function createTile(index: number) {
|
||||||
const x = index % params.columns
|
const x = index % params().columns
|
||||||
const y = Math.floor(index / params.columns)
|
const y = Math.floor(index / params().columns)
|
||||||
const xDiff = (x - position[0]) / 20
|
const xDiff = (x - position()[0]) / 20
|
||||||
const yDiff = (y - position[1]) / 20
|
const yDiff = (y - position()[1]) / 20
|
||||||
const pos = Math.sqrt(xDiff * xDiff + yDiff * yDiff).toFixed(2)
|
const pos = Math.sqrt(xDiff * xDiff + yDiff * yDiff).toFixed(2)
|
||||||
|
|
||||||
function doEffect(posX: number, posY: number) {
|
function doEffect(posX: number, posY: number) {
|
||||||
if (action) return
|
if (action()) return
|
||||||
setPosition([posX, posY])
|
setPosition([posX, posY])
|
||||||
setActve((e) => !e)
|
setActve((e) => !e)
|
||||||
setAction(true)
|
setAction(true)
|
||||||
|
@ -67,15 +71,15 @@ function Grid2() {
|
||||||
}
|
}
|
||||||
const diagonals = [
|
const diagonals = [
|
||||||
pos(0, 0),
|
pos(0, 0),
|
||||||
pos(params.columns, 0),
|
pos(params().columns, 0),
|
||||||
pos(0, params.rows),
|
pos(0, params().rows),
|
||||||
pos(params.columns, params.rows),
|
pos(params().columns, params().rows),
|
||||||
]
|
]
|
||||||
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
setAction(false)
|
setAction(false)
|
||||||
if (active) setCount((e) => e + 1)
|
if (active()) setCount((e) => e + 1)
|
||||||
},
|
},
|
||||||
Math.max(...diagonals) * 1000 + 1000,
|
Math.max(...diagonals) * 1000 + 1000,
|
||||||
)
|
)
|
||||||
|
@ -83,9 +87,8 @@ function Grid2() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
class={classNames("tile", active() ? "active" : "inactive")}
|
||||||
className={classNames("tile", active ? "active" : "inactive")}
|
style={{ "--delay": pos + "s" }}
|
||||||
style={{ "--delay": pos + "s" } as CSSProperties}
|
|
||||||
onClick={() => doEffect(x, y)}
|
onClick={() => doEffect(x, y)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -94,28 +97,24 @@ function Grid2() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="tiles"
|
id="tiles"
|
||||||
style={
|
style={{
|
||||||
{
|
"--columns": params().columns,
|
||||||
"--columns": params.columns,
|
"--rows": params().rows,
|
||||||
"--rows": params.rows,
|
}}
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="center-div">
|
<div class="center-div">
|
||||||
<h1
|
<h1 class={classNames("headline", !active ? "active" : "inactive")}>
|
||||||
className={classNames("headline", !active ? "active" : "inactive")}
|
{sentences[count() % sentences.length]}
|
||||||
>
|
|
||||||
{sentences[count % sentences.length]}
|
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{Array.from(Array(params.quantity), (_tile, index) =>
|
{Array.from(Array(params().quantity), (_tile, index) =>
|
||||||
createTile(index),
|
createTile(index),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [params, position, active, action, count])
|
}, [params, position, active, action, count])
|
||||||
|
|
||||||
return createTiles
|
return createTiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Grid2
|
export default Grid2
|
|
@ -1,5 +1,5 @@
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { ReactNode } from "react"
|
import { JSX } from "solid-js"
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
type,
|
type,
|
||||||
|
@ -12,14 +12,14 @@ function Button({
|
||||||
type: "red" | "orange" | "green" | "gray"
|
type: "red" | "orange" | "green" | "gray"
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
children: ReactNode
|
children: JSX.Element
|
||||||
latching?: boolean
|
latching?: boolean
|
||||||
isLatched?: boolean
|
isLatched?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"font-farro rounded-xl px-8 py-4 text-5xl font-medium duration-100",
|
"font-farro rounded-xl px-8 py-4 text-5xl font-medium duration-100",
|
||||||
disabled
|
disabled
|
||||||
? "border-4 border-dashed"
|
? "border-4 border-dashed"
|
|
@ -1,4 +1,4 @@
|
||||||
import { ReactNode } from "react"
|
import { JSX } from "solid-js"
|
||||||
|
|
||||||
function Icon({
|
function Icon({
|
||||||
src,
|
src,
|
||||||
|
@ -6,20 +6,20 @@ function Icon({
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
src: string
|
src: string
|
||||||
children: ReactNode
|
children: JSX.Element
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="mx-4 mt-4 flex flex-col items-center border-none"
|
class="mx-4 mt-4 flex flex-col items-center border-none"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="pixelart mb-1 box-content w-16 rounded-xl bg-white p-1"
|
class="pixelart mb-1 box-content w-16 rounded-xl bg-white p-1"
|
||||||
src={"/assets/" + src}
|
src={"/assets/" + src}
|
||||||
alt={src}
|
alt={src}
|
||||||
/>
|
/>
|
||||||
<span className="font-semibold">{children}</span>
|
<span class="font-semibold">{children}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
155
leaky-ships/src/components/Lobby/LobbyFrame.tsx
Normal file
155
leaky-ships/src/components/Lobby/LobbyFrame.tsx
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import {
|
||||||
|
faRightFromBracket,
|
||||||
|
faSpinnerThird,
|
||||||
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { JSX, createEffect, createMemo, createSignal } from "solid-js"
|
||||||
|
import { useNavigate } from "solid-start"
|
||||||
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
|
import { useSession } from "~/hooks/useSession"
|
||||||
|
import useSocket from "~/hooks/useSocket"
|
||||||
|
import { socket } from "~/lib/socket"
|
||||||
|
|
||||||
|
import Button from "./Button"
|
||||||
|
import Icon from "./Icon"
|
||||||
|
import Player from "./Player"
|
||||||
|
|
||||||
|
function WithDots({ children }: { children: JSX.Element }) {
|
||||||
|
const [dots, setDots] = createSignal(1)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const interval = setInterval(() => setDots((e) => (e % 3) + 1), 1000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children + " "}
|
||||||
|
{Array.from(Array(dots()), () => ".").join("")}
|
||||||
|
{Array.from(Array(3 - dots()), (_, i) => (
|
||||||
|
<span> </span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
|
||||||
|
const { payload, userStates, full, leave, reset } = useGameProps()
|
||||||
|
const { isConnected } = useSocket()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const session = useSession()
|
||||||
|
const [launchTime, setLaunchTime] = createSignal(3)
|
||||||
|
|
||||||
|
const launching = createMemo(
|
||||||
|
() =>
|
||||||
|
payload?.users.length === 2 &&
|
||||||
|
!userStates.filter((user) => !user.isReady).length,
|
||||||
|
[payload?.users.length, userStates],
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!launching() || launchTime() > 0) return
|
||||||
|
socket.emit("gameState", "starting")
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!launching()) return setLaunchTime(3)
|
||||||
|
if (launchTime() < 0) return
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setLaunchTime((e) => e - 1)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (payload?.game?.id || !isConnected) return
|
||||||
|
socket.emit("update", full)
|
||||||
|
}, [full, payload?.game?.id, isConnected])
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof payload?.game?.state !== "string" ||
|
||||||
|
payload?.game?.state === "lobby"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
navigate("/gamefield")
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="mx-32 flex flex-col self-stretch rounded-3xl bg-gray-400">
|
||||||
|
<div class="flex items-center justify-between border-b-2 border-slate-900">
|
||||||
|
<Icon src="speech_bubble.png">Chat</Icon>
|
||||||
|
<h1 class="font-farro text-5xl font-medium">
|
||||||
|
{launching() ? (
|
||||||
|
<WithDots>
|
||||||
|
{launchTime() < 0
|
||||||
|
? "Game starts"
|
||||||
|
: "Game is starting in " + launchTime()}
|
||||||
|
</WithDots>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{"Game-PIN: "}
|
||||||
|
{isConnected ? (
|
||||||
|
<span class="underline">{payload?.gamePin ?? "----"}</span>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSpinnerThird}
|
||||||
|
// spin={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
<Icon src="gear.png" onClick={openSettings}>
|
||||||
|
Settings
|
||||||
|
</Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-around">
|
||||||
|
{isConnected ? (
|
||||||
|
<>
|
||||||
|
<Player
|
||||||
|
src="player_blue.png"
|
||||||
|
i={0}
|
||||||
|
userId={session.latest?.user?.id}
|
||||||
|
/>
|
||||||
|
<p class="font-farro m-4 text-6xl font-semibold">VS</p>
|
||||||
|
{payload?.users[1] ? (
|
||||||
|
<Player
|
||||||
|
src="player_red.png"
|
||||||
|
i={1}
|
||||||
|
userId={session.latest?.user?.id}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p class="font-farro w-96 text-center text-4xl font-medium">
|
||||||
|
<WithDots>Warte auf Spieler 2</WithDots>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p class="font-farro m-48 text-center text-6xl font-medium">
|
||||||
|
Warte auf Verbindung
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-around border-t-2 border-slate-900 p-4">
|
||||||
|
<Button
|
||||||
|
type={launching() ? "gray" : "red"}
|
||||||
|
disabled={launching()}
|
||||||
|
onClick={() => {
|
||||||
|
leave(async () => {
|
||||||
|
reset()
|
||||||
|
await navigate("/")
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>LEAVE</span>
|
||||||
|
<FontAwesomeIcon icon={faRightFromBracket} className="ml-4 w-12" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LobbyFrame
|
|
@ -6,24 +6,24 @@ import {
|
||||||
faHourglass3,
|
faHourglass3,
|
||||||
faHourglassClock,
|
faHourglassClock,
|
||||||
} from "@fortawesome/pro-solid-svg-icons"
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
|
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
|
||||||
import { socket } from "@lib/socket"
|
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties, useEffect, useMemo, useState } from "react"
|
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||||
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
|
import { socket } from "~/lib/socket"
|
||||||
import Button from "./Button"
|
import Button from "./Button"
|
||||||
|
|
||||||
function HourGlass() {
|
function HourGlass() {
|
||||||
const [count, setCount] = useState(3)
|
const [count, setCount] = createSignal(3)
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const interval = setInterval(() => setCount((e) => (e + 1) % 4), 1000)
|
const interval = setInterval(() => setCount((e) => (e + 1) % 4), 1000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const icon = useMemo(() => {
|
const icon = createMemo(() => {
|
||||||
switch (count) {
|
switch (count()) {
|
||||||
case 0:
|
case 0:
|
||||||
return faHourglass3
|
return faHourglass3
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -38,7 +38,11 @@ function HourGlass() {
|
||||||
}, [count])
|
}, [count])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FontAwesomeIcon icon={icon} className="ml-4 w-12" spin={count === 0} />
|
<FontAwesomeIcon
|
||||||
|
icon={icon()}
|
||||||
|
className="ml-4 w-12"
|
||||||
|
// spin={count() === 0}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,27 +56,24 @@ function Player({
|
||||||
userId?: string
|
userId?: string
|
||||||
}) {
|
}) {
|
||||||
const { payload, userStates, setIsReady } = useGameProps()
|
const { payload, userStates, setIsReady } = useGameProps()
|
||||||
const player = useMemo(() => payload?.users[i], [i, payload?.users])
|
const player = createMemo(() => payload?.users[i], [i, payload?.users])
|
||||||
const { isReady, isConnected } = useMemo(() => userStates[i], [i, userStates])
|
const { isReady, isConnected } = userStates[i]
|
||||||
const primary = useMemo(
|
const primary = createMemo(() => userId && userId === payload?.users[i]?.id)
|
||||||
() => userId && userId === payload?.users[i]?.id,
|
|
||||||
[i, payload?.users, userId],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-96 flex-col items-center gap-4 p-4">
|
<div class="flex w-96 flex-col items-center gap-4 p-4">
|
||||||
<p
|
<p
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"font-farro w-max text-5xl",
|
"font-farro w-max text-5xl",
|
||||||
primary ? "font-semibold" : "font-normal",
|
primary() ? "font-semibold" : "font-normal",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{player?.name ?? "Spieler " + (player?.index === 2 ? "2" : "1")}
|
{player?.name ?? "Spieler " + (player()?.index === 2 ? "2" : "1")}
|
||||||
</p>
|
</p>
|
||||||
<div className="relative">
|
<div class="relative">
|
||||||
<img className="pixelart w-64" src={"/assets/" + src} alt={src} />
|
<img class="pixelart w-64" src={"/assets/" + src} alt={src} />
|
||||||
{primary ? (
|
{primary() ? (
|
||||||
<button className="absolute right-4 top-4 h-14 w-14 rounded-lg border-2 border-dashed border-warn bg-gray-800 bg-opacity-90">
|
<button class="absolute right-4 top-4 h-14 w-14 rounded-lg border-2 border-dashed border-warn bg-gray-800 bg-opacity-90">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="h-full w-full text-warn"
|
className="h-full w-full text-warn"
|
||||||
icon={faCaretDown}
|
icon={faCaretDown}
|
||||||
|
@ -97,12 +98,11 @@ function Player({
|
||||||
Ready
|
Ready
|
||||||
{isReady && isConnected ? (
|
{isReady && isConnected ? (
|
||||||
<FontAwesomeIcon icon={faCheck} className="ml-4 w-12" />
|
<FontAwesomeIcon icon={faCheck} className="ml-4 w-12" />
|
||||||
) : primary ? (
|
) : primary() ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faHandPointer}
|
icon={faHandPointer}
|
||||||
className="ml-4 w-12"
|
className="ml-4 w-12"
|
||||||
style={
|
style={{
|
||||||
{
|
|
||||||
"--fa-bounce-start-scale-x": 1.05,
|
"--fa-bounce-start-scale-x": 1.05,
|
||||||
"--fa-bounce-start-scale-y": 0.95,
|
"--fa-bounce-start-scale-y": 0.95,
|
||||||
"--fa-bounce-jump-scale-x": 0.95,
|
"--fa-bounce-jump-scale-x": 0.95,
|
||||||
|
@ -110,9 +110,8 @@ function Player({
|
||||||
"--fa-bounce-land-scale-x": 1.025,
|
"--fa-bounce-land-scale-x": 1.025,
|
||||||
"--fa-bounce-land-scale-y": 0.975,
|
"--fa-bounce-land-scale-y": 0.975,
|
||||||
"--fa-bounce-height": "-0.125em",
|
"--fa-bounce-height": "-0.125em",
|
||||||
} as CSSProperties
|
}}
|
||||||
}
|
// bounce
|
||||||
bounce
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<HourGlass />
|
<HourGlass />
|
|
@ -1,43 +1,43 @@
|
||||||
import { setGameSetting } from "@components/Gamefield/EventBar"
|
|
||||||
import {
|
import {
|
||||||
faToggleLargeOff,
|
faToggleLargeOff,
|
||||||
faToggleLargeOn,
|
faToggleLargeOn,
|
||||||
} from "@fortawesome/pro-solid-svg-icons"
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { ReactNode, useMemo } from "react"
|
import { JSX, createMemo } from "solid-js"
|
||||||
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
|
import { setGameSetting } from "~/components/Gamefield/EventBar"
|
||||||
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
import { GameSettingKeys } from "../../../interfaces/frontend"
|
import { GameSettingKeys } from "../../../interfaces/frontend"
|
||||||
|
|
||||||
function Setting({
|
function Setting({
|
||||||
children,
|
children,
|
||||||
prop,
|
prop,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: JSX.Element
|
||||||
prop: GameSettingKeys
|
prop: GameSettingKeys
|
||||||
}) {
|
}) {
|
||||||
const { payload, setSetting, full } = useGameProps()
|
const { payload, setSetting, full } = useGameProps()
|
||||||
const state = useMemo(() => payload?.game?.[prop], [payload?.game, prop])
|
const state = createMemo(() => payload?.game?.[prop], [payload?.game, prop])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="flex items-center justify-between" htmlFor={prop}>
|
<label class="flex items-center justify-between" for={prop}>
|
||||||
<span className="col-span-2 w-96 select-none text-5xl text-white drop-shadow-md">
|
<span class="col-span-2 w-96 select-none text-5xl text-white drop-shadow-md">
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-md mx-auto rounded-full px-4 drop-shadow-md transition-all",
|
"text-md mx-auto rounded-full px-4 drop-shadow-md transition-all",
|
||||||
state ? "text-blue-500" : "text-gray-800",
|
state() ? "text-blue-500" : "text-gray-800",
|
||||||
{
|
{
|
||||||
"bg-gray-300 ": state,
|
"bg-gray-300 ": state,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
size="3x"
|
// size="3x"
|
||||||
icon={state ? faToggleLargeOn : faToggleLargeOff}
|
icon={state() ? faToggleLargeOn : faToggleLargeOff}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="bg-none"
|
class="bg-none"
|
||||||
checked={state}
|
checked={state()}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={prop}
|
id={prop}
|
||||||
onChange={() =>
|
onChange={() =>
|
|
@ -1,50 +1,47 @@
|
||||||
import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
|
import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
|
||||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons"
|
import { faXmark } from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import {} from "solid-js"
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
import { socket } from "@lib/socket"
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
import { useCallback } from "react"
|
import { socket } from "~/lib/socket"
|
||||||
import { GameSettings } from "../../../interfaces/frontend"
|
import { GameSettings } from "../../../interfaces/frontend"
|
||||||
import Setting from "./Setting"
|
import Setting from "./Setting"
|
||||||
|
|
||||||
function Settings({ closeSettings }: { closeSettings: () => void }) {
|
function Settings({ closeSettings }: { closeSettings: () => void }) {
|
||||||
const { setSetting, full } = useGameProps()
|
const { setSetting, full } = useGameProps()
|
||||||
|
|
||||||
const gameSetting = useCallback(
|
const gameSetting = (payload: GameSettings) => {
|
||||||
(payload: GameSettings) => {
|
|
||||||
const hash = setSetting(payload)
|
const hash = setSetting(payload)
|
||||||
socket.emit("gameSetting", payload, (newHash) => {
|
socket.emit("gameSetting", payload, (newHash) => {
|
||||||
if (newHash === hash) return
|
if (newHash === hash) return
|
||||||
console.log("hash", hash, newHash)
|
console.log("hash", hash, newHash)
|
||||||
socket.emit("update", full)
|
socket.emit("update", full)
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
[full, setSetting],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/40">
|
<div class="absolute inset-0 flex flex-col items-center justify-center bg-black/40">
|
||||||
<div className="w-full max-w-screen-lg">
|
<div class="w-full max-w-screen-lg">
|
||||||
<div className="mx-16 flex flex-col rounded-3xl border-4 border-slate-800 bg-zinc-500 p-8">
|
<div class="mx-16 flex flex-col rounded-3xl border-4 border-slate-800 bg-zinc-500 p-8">
|
||||||
<div className="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<h1 className="font-farro ml-auto pl-14 text-center text-6xl font-semibold text-white shadow-black drop-shadow-lg">
|
<h1 class="font-farro ml-auto pl-14 text-center text-6xl font-semibold text-white shadow-black drop-shadow-lg">
|
||||||
Settings
|
Settings
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
className="right-6 top-6 ml-auto h-14 w-14"
|
class="right-6 top-6 ml-auto h-14 w-14"
|
||||||
onClick={closeSettings}
|
onClick={closeSettings}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="h-full w-full text-gray-800 drop-shadow-md"
|
className="h-full w-full text-gray-800 drop-shadow-md"
|
||||||
size="3x"
|
// size="3x"
|
||||||
icon={faXmark}
|
icon={faXmark}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 rounded-xl bg-zinc-600 p-8">
|
<div class="mt-8 rounded-xl bg-zinc-600 p-8">
|
||||||
<div className="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
className="right-12 top-8 h-14 w-14"
|
class="right-12 top-8 h-14 w-14"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
gameSetting({
|
gameSetting({
|
||||||
allowSpectators: true,
|
allowSpectators: true,
|
||||||
|
@ -56,12 +53,12 @@ function Settings({ closeSettings }: { closeSettings: () => void }) {
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="h-full w-full text-gray-800 drop-shadow-md"
|
className="h-full w-full text-gray-800 drop-shadow-md"
|
||||||
size="3x"
|
// size="3x"
|
||||||
icon={faRotateLeft}
|
icon={faRotateLeft}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
<Setting prop="allowSpectators">Erlaube Zuschauer</Setting>
|
<Setting prop="allowSpectators">Erlaube Zuschauer</Setting>
|
||||||
<Setting prop="allowSpecials">Erlaube spezial Items</Setting>
|
<Setting prop="allowSpecials">Erlaube spezial Items</Setting>
|
||||||
<Setting prop="allowChat">Erlaube den Chat</Setting>
|
<Setting prop="allowChat">Erlaube den Chat</Setting>
|
|
@ -2,9 +2,9 @@ import classNames from "classnames"
|
||||||
|
|
||||||
function Logo({ small }: { small?: boolean }) {
|
function Logo({ small }: { small?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-center rounded-sm border-x-4 border-y-2 border-shield-gray bg-shield-lightgray md:border-x-8 md:border-y-4">
|
<div class="relative flex flex-col items-center rounded-sm border-x-4 border-y-2 border-shield-gray bg-shield-lightgray md:border-x-8 md:border-y-4">
|
||||||
<h1
|
<h1
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"font-checkpoint mx-16 my-2 flex flex-col gap-2 border-y-2 border-slate-700 text-center text-2xl leading-tight tracking-widest sm:mx-24 sm:my-3 sm:gap-3 sm:border-y-[3px] sm:text-4xl md:mx-36 md:my-4 md:gap-4 md:border-y-4 md:text-5xl",
|
"font-checkpoint mx-16 my-2 flex flex-col gap-2 border-y-2 border-slate-700 text-center text-2xl leading-tight tracking-widest sm:mx-24 sm:my-3 sm:gap-3 sm:border-y-[3px] sm:text-4xl md:mx-36 md:my-4 md:gap-4 md:border-y-4 md:text-5xl",
|
||||||
{ "xl:gap-6 xl:py-2 xl:text-6xl": !small },
|
{ "xl:gap-6 xl:py-2 xl:text-6xl": !small },
|
||||||
)}
|
)}
|
||||||
|
@ -63,14 +63,14 @@ function Screw({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"absolute flex h-3 w-3 flex-col items-center justify-center rounded-full border-[1px] border-neutral-700 bg-neutral-400 sm:h-5 sm:w-5 sm:border-2 md:h-6 md:w-6",
|
"absolute flex h-3 w-3 flex-col items-center justify-center rounded-full border-[1px] border-neutral-700 bg-neutral-400 sm:h-5 sm:w-5 sm:border-2 md:h-6 md:w-6",
|
||||||
{ "xl:h-8 xl:w-8": !small },
|
{ "xl:h-8 xl:w-8": !small },
|
||||||
orientation,
|
orientation,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<hr
|
<hr
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"color w-full border-neutral-500 sm:border-t-2",
|
"color w-full border-neutral-500 sm:border-t-2",
|
||||||
{ "xl:border-t-4": !small },
|
{ "xl:border-t-4": !small },
|
||||||
rotation,
|
rotation,
|
|
@ -1,8 +1,7 @@
|
||||||
import {
|
import { IconDefinition } from "@fortawesome/fontawesome-svg-core"
|
||||||
FontAwesomeIcon,
|
|
||||||
FontAwesomeIconProps,
|
|
||||||
} from "@fortawesome/react-fontawesome"
|
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
|
import { JSX } from "solid-js"
|
||||||
|
import FontAwesomeIcon from "./FontAwesomeIcon"
|
||||||
|
|
||||||
function OptionButton({
|
function OptionButton({
|
||||||
id,
|
id,
|
||||||
|
@ -12,14 +11,14 @@ function OptionButton({
|
||||||
disabled,
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
id: string
|
id: string
|
||||||
icon: FontAwesomeIconProps["icon"]
|
icon: IconDefinition
|
||||||
callback?: () => void
|
callback?: () => void
|
||||||
node?: JSX.Element
|
node?: JSX.Element
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"flex w-full flex-row items-center justify-between rounded-xl py-2 pl-8 pr-4 text-lg text-grayish duration-100 first:mt-4 last:mt-4 sm:py-4 sm:pl-16 sm:pr-8 sm:text-4xl sm:first:mt-8 sm:last:mt-8",
|
"flex w-full flex-row items-center justify-between rounded-xl py-2 pl-8 pr-4 text-lg text-grayish duration-100 first:mt-4 last:mt-4 sm:py-4 sm:pl-16 sm:pr-8 sm:text-4xl sm:first:mt-8 sm:last:mt-8",
|
||||||
!disabled
|
!disabled
|
||||||
? "border-b-4 border-shield-gray bg-voidDark active:border-b-0 active:border-t-4"
|
? "border-b-4 border-shield-gray bg-voidDark active:border-b-0 active:border-t-4"
|
||||||
|
@ -29,7 +28,7 @@ function OptionButton({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={!disabled ? "" : "Please login"}
|
title={!disabled ? "" : "Please login"}
|
||||||
>
|
>
|
||||||
<span className="mx-auto">{node ? node : id}</span>
|
<span class="mx-auto">{node ? node : id}</span>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="ml-2 w-10 text-xl sm:ml-12 sm:text-4xl"
|
className="ml-2 w-10 text-xl sm:ml-12 sm:text-4xl"
|
||||||
icon={icon}
|
icon={icon}
|
|
@ -1,7 +1,7 @@
|
||||||
function profileImg(src: string) {
|
function profileImg(src: string) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
style={{ transform: "scale(1.5)", borderRadius: "100%" }}
|
style={{ transform: "scale(1.5)", "border-radius": "100%" }}
|
||||||
src={src}
|
src={src}
|
||||||
alt="profile picture"
|
alt="profile picture"
|
||||||
/>
|
/>
|
3
leaky-ships/src/entry-client.tsx
Normal file
3
leaky-ships/src/entry-client.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { mount, StartClient } from "solid-start/entry-client"
|
||||||
|
|
||||||
|
mount(() => <StartClient />, document)
|
9
leaky-ships/src/entry-server.tsx
Normal file
9
leaky-ships/src/entry-server.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import {
|
||||||
|
createHandler,
|
||||||
|
renderAsync,
|
||||||
|
StartServer,
|
||||||
|
} from "solid-start/entry-server"
|
||||||
|
|
||||||
|
export default createHandler(
|
||||||
|
renderAsync((event) => <StartServer event={event} />),
|
||||||
|
)
|
|
@ -1,5 +1,5 @@
|
||||||
import { socket } from "@lib/socket"
|
import { createEffect, createSignal } from "solid-js"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { socket } from "~/lib/socket"
|
||||||
import { Draw, DrawLineProps, PlayerEvent, Point } from "../interfaces/frontend"
|
import { Draw, DrawLineProps, PlayerEvent, Point } from "../interfaces/frontend"
|
||||||
import { useDrawProps } from "./useDrawProps"
|
import { useDrawProps } from "./useDrawProps"
|
||||||
|
|
||||||
|
@ -23,17 +23,17 @@ function drawLine({ prevPoint, currentPoint, ctx, color }: Draw) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDraw = () => {
|
export const useDraw = () => {
|
||||||
const [mouseDown, setMouseDown] = useState(false)
|
const [mouseDown, setMouseDown] = createSignal(false)
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
let canvasRef: HTMLCanvasElement
|
||||||
const prevPoint = useRef<null | Point>(null)
|
let prevPoint: null | Point
|
||||||
|
|
||||||
const { color } = useDrawProps()
|
const { color } = useDrawProps()
|
||||||
|
|
||||||
const onMouseDown = () => setMouseDown(true)
|
const onMouseDown = () => setMouseDown(true)
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d")
|
const ctx = canvas.getContext("2d")
|
||||||
|
@ -42,19 +42,19 @@ export const useDraw = () => {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
|
|
||||||
const handler = (e: MouseEvent) => {
|
const handler = (e: MouseEvent) => {
|
||||||
if (!mouseDown) return
|
if (!mouseDown) return
|
||||||
const currentPoint = computePointInCanvas(e)
|
const currentPoint = computePointInCanvas(e)
|
||||||
|
|
||||||
const ctx = canvasRef.current?.getContext("2d")
|
const ctx = canvasRef?.getContext("2d")
|
||||||
if (!ctx || !currentPoint) return
|
if (!ctx || !currentPoint) return
|
||||||
|
|
||||||
drawLine({ ctx, currentPoint, prevPoint: prevPoint.current, color })
|
drawLine({ ctx, currentPoint, prevPoint: prevPoint, color })
|
||||||
prevPoint.current = currentPoint
|
prevPoint = currentPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
const computePointInCanvas = (e: MouseEvent) => {
|
const computePointInCanvas = (e: MouseEvent) => {
|
||||||
|
@ -67,7 +67,7 @@ export const useDraw = () => {
|
||||||
|
|
||||||
const mouseUpHandler = () => {
|
const mouseUpHandler = () => {
|
||||||
setMouseDown(false)
|
setMouseDown(false)
|
||||||
prevPoint.current = null
|
prevPoint = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
|
@ -81,17 +81,17 @@ export const useDraw = () => {
|
||||||
}
|
}
|
||||||
}, [color, mouseDown])
|
}, [color, mouseDown])
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d")
|
const ctx = canvas.getContext("2d")
|
||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
|
|
||||||
const playerEvent = (event: PlayerEvent) => {
|
const playerEvent = (event: PlayerEvent) => {
|
||||||
if (!canvasRef.current?.toDataURL() || event.type !== "connect") return
|
if (!canvasRef?.toDataURL() || event.type !== "connect") return
|
||||||
console.log("sending canvas state")
|
console.log("sending canvas state")
|
||||||
socket.emit("canvas-state", canvasRef.current.toDataURL())
|
socket.emit("canvas-state", canvasRef.toDataURL())
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasStateFromServer = (state: string, index: number) => {
|
const canvasStateFromServer = (state: string, index: number) => {
|
||||||
|
@ -122,5 +122,5 @@ export const useDraw = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return { canvasRef, onMouseDown, clear }
|
return { canvasRef: canvasRef!, onMouseDown, clear }
|
||||||
}
|
}
|
32
leaky-ships/src/hooks/useDrawProps.ts
Normal file
32
leaky-ships/src/hooks/useDrawProps.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { produce } from "immer"
|
||||||
|
import create from "solid-zustand"
|
||||||
|
|
||||||
|
const initialState: {
|
||||||
|
enable: boolean
|
||||||
|
shouldHide: boolean
|
||||||
|
color: string
|
||||||
|
} = {
|
||||||
|
enable: false,
|
||||||
|
shouldHide: false,
|
||||||
|
color: "#b32aa9",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type State = typeof initialState
|
||||||
|
|
||||||
|
export type Action = {
|
||||||
|
setColor: (color: string) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDrawProps = create<State & Action>()((set) => ({
|
||||||
|
...initialState,
|
||||||
|
setColor: (color) =>
|
||||||
|
set(
|
||||||
|
produce((state) => {
|
||||||
|
state.color = color
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
reset: () => {
|
||||||
|
set(initialState)
|
||||||
|
},
|
||||||
|
}))
|
270
leaky-ships/src/hooks/useGameProps.ts
Normal file
270
leaky-ships/src/hooks/useGameProps.ts
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
import { GameState, MoveType } from "@prisma/client"
|
||||||
|
import { produce } from "immer"
|
||||||
|
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
|
||||||
|
import { socket } from "~/lib/socket"
|
||||||
|
import {
|
||||||
|
initlialMouseCursor,
|
||||||
|
initlialTarget,
|
||||||
|
initlialTargetPreview,
|
||||||
|
intersectingShip,
|
||||||
|
targetList,
|
||||||
|
} from "~/lib/utils/helpers"
|
||||||
|
import {
|
||||||
|
GamePropsSchema,
|
||||||
|
PlayerSchema,
|
||||||
|
optionalGamePropsSchema,
|
||||||
|
} from "~/lib/zodSchemas"
|
||||||
|
// import { toast } from "react-toastify"
|
||||||
|
import create from "solid-zustand"
|
||||||
|
|
||||||
|
import {
|
||||||
|
EventBarModes,
|
||||||
|
GameSettings,
|
||||||
|
MouseCursor,
|
||||||
|
MoveDispatchProps,
|
||||||
|
ShipProps,
|
||||||
|
Target,
|
||||||
|
TargetPreview,
|
||||||
|
} from "../interfaces/frontend"
|
||||||
|
|
||||||
|
const initialState: optionalGamePropsSchema & {
|
||||||
|
userStates: {
|
||||||
|
isReady: boolean
|
||||||
|
isConnected: boolean
|
||||||
|
}[]
|
||||||
|
menu: keyof EventBarModes
|
||||||
|
mode: number
|
||||||
|
target: Target
|
||||||
|
targetPreview: TargetPreview
|
||||||
|
mouseCursor: MouseCursor
|
||||||
|
} = {
|
||||||
|
menu: "moves",
|
||||||
|
mode: 0,
|
||||||
|
payload: null,
|
||||||
|
hash: null,
|
||||||
|
target: initlialTarget,
|
||||||
|
targetPreview: initlialTargetPreview,
|
||||||
|
mouseCursor: initlialMouseCursor,
|
||||||
|
userStates: Array.from(Array(2), () => ({
|
||||||
|
isReady: false,
|
||||||
|
isConnected: false,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
export type State = typeof initialState
|
||||||
|
|
||||||
|
export type Action = {
|
||||||
|
DispatchMove: (props: MoveDispatchProps, i: number) => void
|
||||||
|
setTarget: (target: Target | ((i: Target) => Target)) => void
|
||||||
|
setTargetPreview: (
|
||||||
|
targetPreview: TargetPreview | ((i: TargetPreview) => TargetPreview),
|
||||||
|
) => void
|
||||||
|
setMouseCursor: (
|
||||||
|
mouseCursor: MouseCursor | ((i: MouseCursor) => MouseCursor),
|
||||||
|
) => void
|
||||||
|
setPlayer: (payload: { users: PlayerSchema[] }) => string | null
|
||||||
|
setSetting: (settings: GameSettings) => string | null
|
||||||
|
full: (newProps: GamePropsSchema) => void
|
||||||
|
leave: (cb: () => void) => void
|
||||||
|
setIsReady: (payload: { i: number; isReady: boolean }) => void
|
||||||
|
gameState: (newState: GameState) => void
|
||||||
|
setShips: (ships: ShipProps[], index: number) => void
|
||||||
|
removeShip: (props: ShipProps, index: number) => void
|
||||||
|
setIsConnected: (payload: { i: number; isConnected: boolean }) => void
|
||||||
|
reset: () => void
|
||||||
|
setActiveIndex: (i: number, selfIndex: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGameProps = create<State & Action>()((set) => ({
|
||||||
|
...initialState,
|
||||||
|
setActiveIndex: (i, selfIndex) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (!state.payload) return
|
||||||
|
state.payload.activeIndex = i
|
||||||
|
if (i === selfIndex) {
|
||||||
|
state.menu = "moves"
|
||||||
|
state.mode = 0
|
||||||
|
} else {
|
||||||
|
state.menu = "main"
|
||||||
|
state.mode = -1
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
DispatchMove: (move, i) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (!state.payload) return
|
||||||
|
const list = targetList(move, move.type)
|
||||||
|
state.payload.users.map((e) => {
|
||||||
|
if (!e) return e
|
||||||
|
if (i === e.index) e.moves.push(move)
|
||||||
|
else if (move.type !== MoveType.radar)
|
||||||
|
e.hits.push(
|
||||||
|
...list.map(({ x, y }) => ({
|
||||||
|
hit: !!intersectingShip(e.ships, {
|
||||||
|
...move,
|
||||||
|
size: 1,
|
||||||
|
variant: 0,
|
||||||
|
}).fields.length,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setTarget: (dispatch) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (typeof dispatch === "function")
|
||||||
|
state.target = dispatch(state.target)
|
||||||
|
else state.target = dispatch
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setTargetPreview: (dispatch) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (typeof dispatch === "function")
|
||||||
|
state.targetPreview = dispatch(state.targetPreview)
|
||||||
|
else state.targetPreview = dispatch
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setMouseCursor: (dispatch) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (typeof dispatch === "function")
|
||||||
|
state.mouseCursor = dispatch(state.mouseCursor)
|
||||||
|
else state.mouseCursor = dispatch
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setShips: (ships, index) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (!state.payload) return
|
||||||
|
state.payload.users = state.payload.users.map((e) => {
|
||||||
|
if (!e || e.index !== index) return e
|
||||||
|
e.ships = ships
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
removeShip: ({ size, variant, x, y }, index) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
state.payload?.users.map((e) => {
|
||||||
|
if (!e || e.index !== index) return
|
||||||
|
const indexToRemove = e.ships.findIndex(
|
||||||
|
(ship) =>
|
||||||
|
ship.size === size &&
|
||||||
|
ship.variant === variant &&
|
||||||
|
ship.x === x &&
|
||||||
|
ship.y === y,
|
||||||
|
)
|
||||||
|
e.ships.splice(indexToRemove, 1)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setPlayer: (payload) => {
|
||||||
|
let hash: string | null = null
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (!state.payload) return
|
||||||
|
state.payload.users = payload.users
|
||||||
|
const body = getPayloadwithChecksum(state.payload)
|
||||||
|
if (!body.hash) {
|
||||||
|
// toast.warn("Something is wrong... ", {
|
||||||
|
// toastId: "st_wrong",
|
||||||
|
// theme: "colored",
|
||||||
|
// })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash = body.hash
|
||||||
|
state.hash = hash
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return hash
|
||||||
|
},
|
||||||
|
setSetting: (settings) => {
|
||||||
|
let hash: string | null = null
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (!state.payload?.game) return
|
||||||
|
Object.assign(state.payload.game, settings)
|
||||||
|
const body = getPayloadwithChecksum(state.payload)
|
||||||
|
if (!body.hash) {
|
||||||
|
// toast.warn("Something is wrong... ", {
|
||||||
|
// toastId: "st_wrong",
|
||||||
|
// theme: "colored",
|
||||||
|
// })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash = body.hash
|
||||||
|
state.hash = hash
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return hash
|
||||||
|
},
|
||||||
|
full: (newGameProps) =>
|
||||||
|
set((state) => {
|
||||||
|
if (state.hash === newGameProps.hash) {
|
||||||
|
console.log("Everything up to date.")
|
||||||
|
} else {
|
||||||
|
console.log("Update was needed.", state.hash, newGameProps.hash)
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.payload?.game?.id &&
|
||||||
|
state.payload?.game?.id !== newGameProps.payload?.game?.id
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
"Different gameId detected on update: ",
|
||||||
|
state.payload?.game?.id,
|
||||||
|
newGameProps.payload?.game?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newGameProps
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}),
|
||||||
|
leave: (cb) => {
|
||||||
|
socket.emit("leave", (ack) => {
|
||||||
|
if (!ack) {
|
||||||
|
// toast.error("Something is wrong...")
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setIsReady: ({ i, isReady }) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
state.userStates[i].isReady = isReady
|
||||||
|
state.userStates[i].isConnected = true
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
gameState: (newState: GameState) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
if (!state.payload?.game) return
|
||||||
|
state.payload.game.state = newState
|
||||||
|
state.userStates = state.userStates.map((e) => ({
|
||||||
|
...e,
|
||||||
|
isReady: false,
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setIsConnected: ({ i, isConnected }) =>
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
state.userStates[i].isConnected = isConnected
|
||||||
|
if (isConnected) return
|
||||||
|
state.userStates[i].isReady = false
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
reset: () => {
|
||||||
|
set(initialState)
|
||||||
|
},
|
||||||
|
}))
|
|
@ -1,12 +1,13 @@
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "~/hooks/useSession"
|
||||||
|
|
||||||
import { useGameProps } from "./useGameProps"
|
import { useGameProps } from "./useGameProps"
|
||||||
|
|
||||||
function useIndex() {
|
function useIndex() {
|
||||||
const { payload } = useGameProps()
|
const { payload } = useGameProps()
|
||||||
const { data: session } = useSession()
|
const session = useSession()
|
||||||
|
|
||||||
const selfIndex =
|
const selfIndex =
|
||||||
payload?.users.findIndex((e) => e?.id === session?.user.id) ?? -1
|
payload?.users.findIndex((e) => e?.id === session.latest?.user.id) ?? -1
|
||||||
const activeIndex = payload?.activeIndex ?? -1
|
const activeIndex = payload?.activeIndex ?? -1
|
||||||
const isActiveIndex = selfIndex >= 0 && payload?.activeIndex === selfIndex
|
const isActiveIndex = selfIndex >= 0 && payload?.activeIndex === selfIndex
|
||||||
const selfUser = payload?.users[selfIndex]
|
const selfUser = payload?.users[selfIndex]
|
12
leaky-ships/src/hooks/useSession.ts
Normal file
12
leaky-ships/src/hooks/useSession.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { getSession } from "@auth/solid-start"
|
||||||
|
import { createServerData$ } from "solid-start/server"
|
||||||
|
import { authOptions } from "~/server/auth"
|
||||||
|
|
||||||
|
export const useSession = () => {
|
||||||
|
return createServerData$(
|
||||||
|
async (_, { request }) => {
|
||||||
|
return await getSession(request, authOptions)
|
||||||
|
},
|
||||||
|
{ key: () => ["auth_user"] },
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useMemo } from "react"
|
import { createMemo } from "solid-js"
|
||||||
import { ShipProps } from "../interfaces/frontend"
|
import { ShipProps } from "../interfaces/frontend"
|
||||||
import { useGameProps } from "./useGameProps"
|
import { useGameProps } from "./useGameProps"
|
||||||
import useIndex from "./useIndex"
|
import useIndex from "./useIndex"
|
||||||
|
@ -7,19 +7,12 @@ function useShips() {
|
||||||
const gameProps = useGameProps()
|
const gameProps = useGameProps()
|
||||||
const { selfIndex } = useIndex()
|
const { selfIndex } = useIndex()
|
||||||
|
|
||||||
const ships = useMemo(
|
const ships = createMemo(
|
||||||
() =>
|
() =>
|
||||||
gameProps.payload?.users.find((e) => e?.index === selfIndex)?.ships ?? [],
|
gameProps.payload?.users.find((e) => e?.index === selfIndex)?.ships ?? [],
|
||||||
[gameProps.payload?.users, selfIndex],
|
|
||||||
)
|
|
||||||
const setShips = useCallback(
|
|
||||||
(ships: ShipProps[]) => gameProps.setShips(ships, selfIndex),
|
|
||||||
[gameProps, selfIndex],
|
|
||||||
)
|
|
||||||
const removeShip = useCallback(
|
|
||||||
(ship: ShipProps) => gameProps.removeShip(ship, selfIndex),
|
|
||||||
[gameProps, selfIndex],
|
|
||||||
)
|
)
|
||||||
|
const setShips = (ships: ShipProps[]) => gameProps.setShips(ships, selfIndex)
|
||||||
|
const removeShip = (ship: ShipProps) => gameProps.removeShip(ship, selfIndex)
|
||||||
|
|
||||||
return { ships, setShips, removeShip }
|
return { ships, setShips, removeShip }
|
||||||
}
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
import { socket } from "@lib/socket"
|
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
|
||||||
import status from "http-status"
|
import status from "http-status"
|
||||||
import { useRouter } from "next/router"
|
// import { toast } from "react-toastify"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||||
import { toast } from "react-toastify"
|
import { useNavigate } from "solid-start"
|
||||||
|
import { socket } from "~/lib/socket"
|
||||||
|
import { GamePropsSchema } from "~/lib/zodSchemas"
|
||||||
|
// import { isAuthenticated } from "~/routes/start"
|
||||||
import { GameSettings, PlayerEvent } from "../interfaces/frontend"
|
import { GameSettings, PlayerEvent } from "../interfaces/frontend"
|
||||||
import { isAuthenticated } from "../pages/start"
|
|
||||||
import { useGameProps } from "./useGameProps"
|
import { useGameProps } from "./useGameProps"
|
||||||
import useIndex from "./useIndex"
|
import useIndex from "./useIndex"
|
||||||
|
|
||||||
/** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
|
/** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
|
||||||
function useSocket() {
|
function useSocket() {
|
||||||
const [isConnectedState, setIsConnectedState] = useState(false)
|
const [isConnectedState, setIsConnectedState] = createSignal(false)
|
||||||
const { selfIndex } = useIndex()
|
const { selfIndex } = useIndex()
|
||||||
const {
|
const {
|
||||||
payload,
|
payload,
|
||||||
|
@ -26,42 +26,42 @@ function useSocket() {
|
||||||
DispatchMove,
|
DispatchMove,
|
||||||
setShips,
|
setShips,
|
||||||
} = useGameProps()
|
} = useGameProps()
|
||||||
const router = useRouter()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const isConnected = useMemo(
|
const isConnected = createMemo(
|
||||||
() =>
|
() =>
|
||||||
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
|
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState(),
|
||||||
[selfIndex, isConnectedState, userStates],
|
[selfIndex, isConnectedState(), userStates],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
if (selfIndex < 0) return
|
if (selfIndex < 0) return
|
||||||
setIsConnected({
|
setIsConnected({
|
||||||
i: selfIndex,
|
i: selfIndex,
|
||||||
isConnected: isConnectedState,
|
isConnected: isConnectedState(),
|
||||||
})
|
})
|
||||||
}, [selfIndex, isConnectedState, setIsConnected])
|
}, [selfIndex, isConnectedState(), setIsConnected])
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
console.log("connected")
|
console.log("connected")
|
||||||
toast.dismiss("connect_error")
|
// toast.dismiss("connect_error")
|
||||||
setIsConnectedState(true)
|
setIsConnectedState(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectError = (error: Error) => {
|
const connectError = (error: Error) => {
|
||||||
console.log("Connection error:", error.message)
|
console.log("Connection error:", error.message)
|
||||||
if (error.message === status["403"]) router.push("/")
|
if (error.message === status["403"]) navigate("/")
|
||||||
if (error.message !== "xhr poll error") return
|
if (error.message !== "xhr poll error") return
|
||||||
const toastId = "connect_error"
|
// const toastId = "connect_error"
|
||||||
const isActive = toast.isActive(toastId)
|
// const isActive = toast.isActive(toastId)
|
||||||
console.log(toastId, isActive)
|
// console.log(toastId, isActive)
|
||||||
if (isActive)
|
// if (isActive)
|
||||||
toast.update(toastId, {
|
// toast.update(toastId, {
|
||||||
autoClose: 5000,
|
// autoClose: 5000,
|
||||||
})
|
// })
|
||||||
else
|
// else
|
||||||
toast.warn("Es gibt Probleme mit der Echtzeitverbindung.", { toastId })
|
// toast.warn("Es gibt Probleme mit der Echtzeitverbindung.", { toastId })
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerEvent = (event: PlayerEvent) => {
|
const playerEvent = (event: PlayerEvent) => {
|
||||||
|
@ -94,7 +94,7 @@ function useSocket() {
|
||||||
message = "Not defined yet."
|
message = "Not defined yet."
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
toast.info(message, { toastId: message })
|
// toast.info(message, { toastId: message })
|
||||||
if (type === "disconnect") return
|
if (type === "disconnect") return
|
||||||
const { payload, hash } = event
|
const { payload, hash } = event
|
||||||
const newHash = setPlayer(payload)
|
const newHash = setPlayer(payload)
|
||||||
|
@ -147,45 +147,32 @@ function useSocket() {
|
||||||
socket.off("ships", setShips)
|
socket.off("ships", setShips)
|
||||||
socket.off("disconnect", disconnect)
|
socket.off("disconnect", disconnect)
|
||||||
}
|
}
|
||||||
}, [
|
})
|
||||||
DispatchMove,
|
|
||||||
full,
|
|
||||||
gameState,
|
|
||||||
router,
|
|
||||||
selfIndex,
|
|
||||||
setActiveIndex,
|
|
||||||
setIsConnected,
|
|
||||||
setIsReady,
|
|
||||||
setPlayer,
|
|
||||||
setSetting,
|
|
||||||
setShips,
|
|
||||||
userStates,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
createEffect(() => {
|
||||||
if (!payload?.game?.id) {
|
if (!payload?.game?.id) {
|
||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
fetch("/api/game/running", {
|
fetch("/api/game/running", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
})
|
})
|
||||||
.then(isAuthenticated)
|
// .then(isAuthenticated)
|
||||||
.then((game) => GamePropsSchema.parse(game))
|
.then((game) => GamePropsSchema.parse(game))
|
||||||
.then((res) => full(res))
|
.then((res) => full(res))
|
||||||
.catch((e) => console.log(e))
|
.catch((e) => console.log(e))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isConnected) return
|
if (isConnected()) return
|
||||||
socket.connect()
|
socket.connect()
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
socket.volatile.emit("ping", () => {
|
socket.volatile.emit("ping", () => {
|
||||||
const duration = Date.now() - start
|
const duration = Date.now() - start
|
||||||
console.log("ping", duration)
|
console.log("ping", duration)
|
||||||
})
|
})
|
||||||
}, [full, isConnected, payload?.game?.id])
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isConnected:
|
isConnected:
|
||||||
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
|
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
import { Session } from "@auth/core/types"
|
||||||
import { GameState } from "@prisma/client"
|
import { GameState } from "@prisma/client"
|
||||||
import type { Server as HTTPServer } from "http"
|
import type { Server as HTTPServer } from "http"
|
||||||
import type { Socket as NetSocket } from "net"
|
import type { Socket as NetSocket } from "net"
|
||||||
import type { NextApiResponse } from "next"
|
|
||||||
import { Session } from "next-auth"
|
|
||||||
import type {
|
import type {
|
||||||
Server as IOServer,
|
Server as IOServer,
|
||||||
Server,
|
Server,
|
||||||
Socket as SocketforServer,
|
Socket as SocketforServer,
|
||||||
} from "socket.io"
|
} from "socket.io"
|
||||||
import type { Socket as SocketforClient } from "socket.io-client"
|
import type { Socket as SocketforClient } from "socket.io-client"
|
||||||
|
import { GamePropsSchema } from "~/lib/zodSchemas"
|
||||||
import {
|
import {
|
||||||
DrawLineProps,
|
DrawLineProps,
|
||||||
GameSettings,
|
GameSettings,
|
||||||
|
@ -26,7 +25,7 @@ interface SocketWithIO extends NetSocket {
|
||||||
server: SocketServer
|
server: SocketServer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NextApiResponseWithSocket extends NextApiResponse {
|
export interface RequestWithSocket extends Request {
|
||||||
socket: SocketWithIO
|
socket: SocketWithIO
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons"
|
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { PlayerSchema } from "@lib/zodSchemas"
|
|
||||||
import { MoveType, Orientation } from "@prisma/client"
|
import { MoveType, Orientation } from "@prisma/client"
|
||||||
|
import { PlayerSchema } from "~/lib/zodSchemas"
|
||||||
|
|
||||||
export interface Position {
|
export interface Position {
|
||||||
x: number
|
x: number
|
|
@ -1,4 +1,4 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
import { APIEvent } from "solid-start"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import sendError from "./sendError"
|
import sendError from "./sendError"
|
||||||
|
|
||||||
|
@ -6,13 +6,13 @@ const pinBodySchema = z.object({
|
||||||
pin: z.string(),
|
pin: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getPinFromBody<T>(req: NextApiRequest, res: NextApiResponse<T>) {
|
async function getPinFromBody<T>(request: APIEvent["request"]) {
|
||||||
try {
|
try {
|
||||||
const body = JSON.parse(req.body)
|
const body = request.json()
|
||||||
const { pin } = pinBodySchema.parse(body)
|
const { pin } = pinBodySchema.parse(body)
|
||||||
return pin
|
return pin
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
sendError(req, res, {
|
sendError(request, {
|
||||||
message: "No pin in request body!",
|
message: "No pin in request body!",
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
solved: true,
|
solved: true,
|
|
@ -1,7 +1,7 @@
|
||||||
import colors, { Color } from "colors"
|
import colors, { Color } from "colors"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { IncomingMessage } from "http"
|
import { IncomingMessage } from "http"
|
||||||
import { NextApiRequest } from "next"
|
import { APIEvent } from "solid-start/api"
|
||||||
|
|
||||||
colors.enable()
|
colors.enable()
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ async function logStartup() {
|
||||||
async function logging(
|
async function logging(
|
||||||
message: string,
|
message: string,
|
||||||
types: Logging[],
|
types: Logging[],
|
||||||
req?: NextApiRequest | IncomingMessage,
|
request?: APIEvent["request"] | IncomingMessage,
|
||||||
) {
|
) {
|
||||||
if (!started) await logStartup()
|
if (!started) await logStartup()
|
||||||
const messages = { console: message, file: message }
|
const messages = { console: message, file: message }
|
||||||
|
@ -55,14 +55,16 @@ async function logging(
|
||||||
messages.console =
|
messages.console =
|
||||||
`[${new Date().toString().slice(0, 33)}] ` + messages.console
|
`[${new Date().toString().slice(0, 33)}] ` + messages.console
|
||||||
messages.file = `[${new Date().toString().slice(0, 33)}] ` + messages.file
|
messages.file = `[${new Date().toString().slice(0, 33)}] ` + messages.file
|
||||||
if (req) {
|
if (request) {
|
||||||
const forwardedFor: any = req.headers["x-forwarded-for"]
|
const forwardedFor = request.headers
|
||||||
const ip = (forwardedFor || "127.0.0.1, 192.168.178.1").split(",")
|
console.log(JSON.stringify(forwardedFor))
|
||||||
const route = req.url
|
// ("x-forwarded-for")
|
||||||
messages.console = [ip[0].yellow, route?.green, messages.console].join(
|
// const ip = (forwardedFor || "127.0.0.1, 192.168.178.1").split(",")
|
||||||
" - ",
|
// const route = request.url
|
||||||
)
|
// messages.console = [ip[0].yellow, route?.green, messages.console].join(
|
||||||
messages.file = [ip[0], route, messages.file].join(" - ")
|
// " - ",
|
||||||
|
// )
|
||||||
|
// messages.file = [ip[0], route, messages.file].join(" - ")
|
||||||
}
|
}
|
||||||
await fs.promises.appendFile("log/log.txt", messages.file + "\n")
|
await fs.promises.appendFile("log/log.txt", messages.file + "\n")
|
||||||
console.log(messages.console)
|
console.log(messages.console)
|
|
@ -1,20 +1,19 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
import { APIEvent, json } from "solid-start"
|
||||||
import { rejectionError } from "./errors"
|
import { rejectionError } from "./errors"
|
||||||
import logging from "./logging"
|
import logging from "./logging"
|
||||||
|
|
||||||
export default function sendError<T>(
|
export default function sendError<T>(
|
||||||
req: NextApiRequest,
|
request: APIEvent["request"],
|
||||||
res: NextApiResponse<T>,
|
|
||||||
err: rejectionError | Error,
|
err: rejectionError | Error,
|
||||||
) {
|
) {
|
||||||
// If something went wrong, let the client know with status 500
|
|
||||||
res.status("statusCode" in err ? err.statusCode : 500).end()
|
|
||||||
logging(
|
logging(
|
||||||
err.message,
|
err.message,
|
||||||
"type" in err && err.type
|
"type" in err && err.type
|
||||||
? err.type
|
? err.type
|
||||||
: ["solved" in err && err.solved ? "debug" : "error"],
|
: ["solved" in err && err.solved ? "debug" : "error"],
|
||||||
req,
|
request,
|
||||||
)
|
)
|
||||||
if ("name" in err) console.log(err)
|
if ("name" in err) console.log(err)
|
||||||
|
// If something went wrong, let the client know with status 500
|
||||||
|
return json(null, { status: "statusCode" in err ? err.statusCode : 500 })
|
||||||
}
|
}
|
22
leaky-ships/src/lib/backend/sendResponse.ts
Normal file
22
leaky-ships/src/lib/backend/sendResponse.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { APIEvent, json, redirect } from "solid-start/api"
|
||||||
|
import logging, { Logging } from "./logging"
|
||||||
|
|
||||||
|
export interface Result<T> {
|
||||||
|
message: string
|
||||||
|
statusCode?: number
|
||||||
|
body?: T
|
||||||
|
type?: Logging[]
|
||||||
|
redirectUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function sendResponse<T>(
|
||||||
|
request: APIEvent["request"],
|
||||||
|
result: Result<T>,
|
||||||
|
) {
|
||||||
|
if (result.redirectUrl) {
|
||||||
|
return redirect(result.redirectUrl)
|
||||||
|
} else {
|
||||||
|
logging(result.message, result.type ?? ["debug"], request)
|
||||||
|
return json(result.body, { status: result.statusCode ?? 200 })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { count } from "@components/Gamefield/Gamefield"
|
|
||||||
import { Orientation } from "@prisma/client"
|
import { Orientation } from "@prisma/client"
|
||||||
|
import { count } from "~/components/Gamefield/Gamefield"
|
||||||
import type {
|
import type {
|
||||||
Hit,
|
Hit,
|
||||||
IndexedPosition,
|
IndexedPosition,
|
53
leaky-ships/src/root.tsx
Normal file
53
leaky-ships/src/root.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// @refresh reload
|
||||||
|
import "@fortawesome/fontawesome-svg-core/styles.css"
|
||||||
|
import { Suspense } from "solid-js"
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
ErrorBoundary,
|
||||||
|
FileRoutes,
|
||||||
|
Head,
|
||||||
|
Html,
|
||||||
|
Link,
|
||||||
|
Meta,
|
||||||
|
Routes,
|
||||||
|
Scripts,
|
||||||
|
Title,
|
||||||
|
} from "solid-start"
|
||||||
|
import "./styles/App.scss"
|
||||||
|
import "./styles/globals.scss"
|
||||||
|
import "./styles/grid.scss"
|
||||||
|
import "./styles/grid2.scss"
|
||||||
|
import "./styles/root.css"
|
||||||
|
|
||||||
|
export default function Root() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head>
|
||||||
|
<Title>Leaky Ships</Title>
|
||||||
|
<Meta charset="utf-8" />
|
||||||
|
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<Link rel="manifest" href="/manifest.json" />
|
||||||
|
<Link rel="icon" href="/favicon.ico" />
|
||||||
|
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta name="theme-color" content="#000000" />
|
||||||
|
<Meta
|
||||||
|
name="description"
|
||||||
|
content="Battleship web app with react frontend and ASP .NET backend"
|
||||||
|
/>
|
||||||
|
<Link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
|
</Head>
|
||||||
|
<Body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<Suspense fallback={<div>Loading</div>}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Routes>
|
||||||
|
<FileRoutes />
|
||||||
|
</Routes>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Suspense>
|
||||||
|
<Scripts />
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
4
leaky-ships/src/routes/api/auth/[...solidauth].ts
Normal file
4
leaky-ships/src/routes/api/auth/[...solidauth].ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { SolidAuth } from "@auth/solid-start"
|
||||||
|
import { authOptions } from "~/server/auth"
|
||||||
|
|
||||||
|
export const { GET, POST } = SolidAuth(authOptions)
|
44
leaky-ships/src/routes/api/game/[id].ts
Normal file
44
leaky-ships/src/routes/api/game/[id].ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { getSession } from "@auth/solid-start"
|
||||||
|
import { Game } from "@prisma/client"
|
||||||
|
import { APIEvent } from "solid-start"
|
||||||
|
import { rejectionErrors } from "~/lib/backend/errors"
|
||||||
|
import sendResponse from "~/lib/backend/sendResponse"
|
||||||
|
import prisma from "~/lib/prisma"
|
||||||
|
import { authOptions } from "~/server/auth"
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
game: Game
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: APIEvent["request"]) {
|
||||||
|
const body = request.json() as any //TODO
|
||||||
|
const gameId = body.query.id
|
||||||
|
const session = await getSession(request, authOptions)
|
||||||
|
|
||||||
|
if (!session?.user || typeof gameId !== "string") {
|
||||||
|
return sendResponse(request, rejectionErrors.unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
let game: Game | null
|
||||||
|
switch (request.method) {
|
||||||
|
case "DELETE":
|
||||||
|
game = await prisma.game.delete({
|
||||||
|
where: { id: gameId },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
game = await prisma.game.findFirst({
|
||||||
|
where: { id: gameId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!game) {
|
||||||
|
return sendResponse(request, rejectionErrors.gameNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse(request, {
|
||||||
|
message: "Here is the game.",
|
||||||
|
body: { game },
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,20 +1,16 @@
|
||||||
import sendResponse from "@backend/sendResponse"
|
import { getSession } from "@auth/solid-start"
|
||||||
import { rejectionErrors } from "@lib/backend/errors"
|
import { APIEvent } from "solid-start"
|
||||||
import prisma from "@lib/prisma"
|
import { rejectionErrors } from "~/lib/backend/errors"
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
import sendResponse from "~/lib/backend/sendResponse"
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
import prisma from "~/lib/prisma"
|
||||||
import { getServerSession } from "next-auth"
|
import { authOptions } from "~/server/auth"
|
||||||
import { authOptions } from "../auth/[...nextauth]"
|
|
||||||
import { composeBody, gameSelects, getAnyRunningGame } from "./running"
|
import { composeBody, gameSelects, getAnyRunningGame } from "./running"
|
||||||
|
|
||||||
export default async function create(
|
export async function POST(request: APIEvent["request"]) {
|
||||||
req: NextApiRequest,
|
const session = await getSession(request, authOptions)
|
||||||
res: NextApiResponse<GamePropsSchema>,
|
|
||||||
) {
|
|
||||||
const session = await getServerSession(req, res, authOptions)
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return sendResponse(req, res, rejectionErrors.unauthorized)
|
return sendResponse(request, rejectionErrors.unauthorized)
|
||||||
}
|
}
|
||||||
const { email, id } = session.user
|
const { email, id } = session.user
|
||||||
|
|
||||||
|
@ -27,7 +23,7 @@ export default async function create(
|
||||||
|
|
||||||
let game = await getAnyRunningGame(id)
|
let game = await getAnyRunningGame(id)
|
||||||
if (game) {
|
if (game) {
|
||||||
return sendResponse(req, res, {
|
return sendResponse(request, {
|
||||||
redirectUrl: "/api/game/running",
|
redirectUrl: "/api/game/running",
|
||||||
message: "Running game already exists.",
|
message: "Running game already exists.",
|
||||||
})
|
})
|
||||||
|
@ -57,7 +53,7 @@ export default async function create(
|
||||||
|
|
||||||
const body = composeBody(game)
|
const body = composeBody(game)
|
||||||
|
|
||||||
return sendResponse(req, res, {
|
return sendResponse(request, {
|
||||||
message: `User <${email}> created game: ${game.id}`,
|
message: `User <${email}> created game: ${game.id}`,
|
||||||
statusCode: created ? 201 : 200,
|
statusCode: created ? 201 : 200,
|
||||||
body,
|
body,
|
|
@ -1,24 +1,20 @@
|
||||||
import sendError from "@backend/sendError"
|
import { getSession } from "@auth/solid-start"
|
||||||
import sendResponse from "@backend/sendResponse"
|
import { APIEvent } from "solid-start"
|
||||||
import { rejectionErrors } from "@lib/backend/errors"
|
import { rejectionErrors } from "~/lib/backend/errors"
|
||||||
import getPinFromBody from "@lib/backend/getPinFromBody"
|
import getPinFromBody from "~/lib/backend/getPinFromBody"
|
||||||
import logging from "@lib/backend/logging"
|
import logging from "~/lib/backend/logging"
|
||||||
import prisma from "@lib/prisma"
|
import sendError from "~/lib/backend/sendError"
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
import sendResponse from "~/lib/backend/sendResponse"
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
import prisma from "~/lib/prisma"
|
||||||
import { getServerSession } from "next-auth"
|
import { authOptions } from "~/server/auth"
|
||||||
import { authOptions } from "../auth/[...nextauth]"
|
|
||||||
import { composeBody, gameSelects } from "./running"
|
import { composeBody, gameSelects } from "./running"
|
||||||
|
|
||||||
export default async function join(
|
export async function POST(request: APIEvent["request"]) {
|
||||||
req: NextApiRequest,
|
const session = await getSession(request, authOptions)
|
||||||
res: NextApiResponse<GamePropsSchema>,
|
const pin = await getPinFromBody(request)
|
||||||
) {
|
|
||||||
const session = await getServerSession(req, res, authOptions)
|
|
||||||
const pin = await getPinFromBody(req, res)
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return sendResponse(req, res, rejectionErrors.unauthorized)
|
return sendResponse(request, rejectionErrors.unauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, id } = session.user
|
const { email, id } = session.user
|
||||||
|
@ -32,7 +28,7 @@ export default async function join(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!game) {
|
if (!game) {
|
||||||
return sendResponse(req, res, {
|
return sendResponse(request, {
|
||||||
message: "Spiel existiert nicht",
|
message: "Spiel existiert nicht",
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
type: ["infoCyan"],
|
type: ["infoCyan"],
|
||||||
|
@ -53,7 +49,7 @@ export default async function join(
|
||||||
...gameSelects,
|
...gameSelects,
|
||||||
})
|
})
|
||||||
if (games.length) {
|
if (games.length) {
|
||||||
return sendResponse(req, res, {
|
return sendResponse(request, {
|
||||||
message: "Spieler ist bereits in Spiel!",
|
message: "Spieler ist bereits in Spiel!",
|
||||||
redirectUrl: "/api/game/running",
|
redirectUrl: "/api/game/running",
|
||||||
type: ["infoCyan"],
|
type: ["infoCyan"],
|
||||||
|
@ -73,7 +69,7 @@ export default async function join(
|
||||||
|
|
||||||
const body = composeBody(user_Game.game)
|
const body = composeBody(user_Game.game)
|
||||||
|
|
||||||
return sendResponse(req, res, {
|
return sendResponse(request, {
|
||||||
message: `User <${email}> joined game: ${game.id}`,
|
message: `User <${email}> joined game: ${game.id}`,
|
||||||
body,
|
body,
|
||||||
type: ["debug", "infoCyan"],
|
type: ["debug", "infoCyan"],
|
||||||
|
@ -82,8 +78,8 @@ export default async function join(
|
||||||
await logging(
|
await logging(
|
||||||
"HERE".red + err.code + err.meta + err.message,
|
"HERE".red + err.code + err.meta + err.message,
|
||||||
["error"],
|
["error"],
|
||||||
req,
|
request,
|
||||||
)
|
)
|
||||||
throw sendError(req, res, rejectionErrors.gameNotFound)
|
throw sendError(request, rejectionErrors.gameNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import sendResponse from "@backend/sendResponse"
|
import { getSession } from "@auth/solid-start"
|
||||||
import { rejectionErrors } from "@lib/backend/errors"
|
import { type APIEvent } from "solid-start/api"
|
||||||
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
|
import { rejectionErrors } from "~/lib/backend/errors"
|
||||||
import prisma from "@lib/prisma"
|
import sendResponse from "~/lib/backend/sendResponse"
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
import prisma from "~/lib/prisma"
|
||||||
import { getServerSession } from "next-auth"
|
import { GamePropsSchema } from "~/lib/zodSchemas"
|
||||||
import { authOptions } from "../auth/[...nextauth]"
|
import { authOptions } from "~/server/auth"
|
||||||
|
|
||||||
export const gameSelects = {
|
export const gameSelects = {
|
||||||
select: {
|
select: {
|
||||||
|
@ -123,14 +123,11 @@ export function composeBody(
|
||||||
return getPayloadwithChecksum(payload)
|
return getPayloadwithChecksum(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function running(
|
export async function POST({ request }: APIEvent) {
|
||||||
req: NextApiRequest,
|
const session = await getSession(request, authOptions)
|
||||||
res: NextApiResponse<GamePropsSchema>,
|
|
||||||
) {
|
|
||||||
const session = await getServerSession(req, res, authOptions)
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return sendResponse(req, res, rejectionErrors.unauthorized)
|
return sendResponse(request, rejectionErrors.unauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, id } = session.user
|
const { email, id } = session.user
|
||||||
|
@ -138,7 +135,7 @@ export default async function running(
|
||||||
const game = await getAnyRunningGame(id)
|
const game = await getAnyRunningGame(id)
|
||||||
|
|
||||||
if (!game)
|
if (!game)
|
||||||
return sendResponse(req, res, {
|
return sendResponse(request, {
|
||||||
message: `User <${email}> is in no game.`,
|
message: `User <${email}> is in no game.`,
|
||||||
statusCode: 204,
|
statusCode: 204,
|
||||||
type: ["debug", "infoCyan"],
|
type: ["debug", "infoCyan"],
|
||||||
|
@ -146,7 +143,7 @@ export default async function running(
|
||||||
|
|
||||||
const body = composeBody(game)
|
const body = composeBody(game)
|
||||||
|
|
||||||
return sendResponse(req, res, {
|
return sendResponse(request, {
|
||||||
message: `User <${email}> asked for game: ${game.id}`,
|
message: `User <${email}> asked for game: ${game.id}`,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body,
|
body,
|
|
@ -1,15 +1,12 @@
|
||||||
import logging from "@lib/backend/logging"
|
import { getSession } from "@auth/solid-start"
|
||||||
import prisma from "@lib/prisma"
|
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
|
||||||
import colors from "colors"
|
import colors from "colors"
|
||||||
import status from "http-status"
|
import status from "http-status"
|
||||||
import { NextApiRequest } from "next"
|
|
||||||
import { getSession } from "next-auth/react"
|
|
||||||
import { Server } from "socket.io"
|
import { Server } from "socket.io"
|
||||||
import {
|
import { RequestWithSocket, sServer } from "~/interfaces/NextApiSocket"
|
||||||
NextApiResponseWithSocket,
|
import logging from "~/lib/backend/logging"
|
||||||
sServer,
|
import prisma from "~/lib/prisma"
|
||||||
} from "../../interfaces/NextApiSocket"
|
import { GamePropsSchema } from "~/lib/zodSchemas"
|
||||||
|
import { authOptions } from "~/server/auth"
|
||||||
import {
|
import {
|
||||||
composeBody,
|
composeBody,
|
||||||
gameSelects,
|
gameSelects,
|
||||||
|
@ -19,29 +16,25 @@ import {
|
||||||
|
|
||||||
colors.enable()
|
colors.enable()
|
||||||
|
|
||||||
const SocketHandler = async (
|
export async function GET(request: RequestWithSocket) {
|
||||||
req: NextApiRequest,
|
if (request.socket.server.io) {
|
||||||
res: NextApiResponseWithSocket,
|
logging("Socket is already running " + request.url, ["infoCyan"], request)
|
||||||
) => {
|
|
||||||
if (res.socket.server.io) {
|
|
||||||
logging("Socket is already running " + req.url, ["infoCyan"], req)
|
|
||||||
} else {
|
} else {
|
||||||
logging("Socket is initializing " + req.url, ["infoCyan"], req)
|
logging("Socket is initializing " + request.url, ["infoCyan"], request)
|
||||||
const io: sServer = new Server(res.socket.server, {
|
const io: sServer = new Server(request.socket.server, {
|
||||||
path: "/api/ws",
|
path: "/api/ws",
|
||||||
cors: {
|
cors: {
|
||||||
origin: "https://leaky-ships.mal-noh.de",
|
origin: "https://leaky-ships.mal-noh.de",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
res.socket.server.io = io
|
request.socket.server.io = io
|
||||||
|
|
||||||
// io.use(authenticate)
|
// io.use(authenticate)
|
||||||
io.use(async (socket, next) => {
|
io.use(async (socket, next) => {
|
||||||
try {
|
try {
|
||||||
const session = await getSession({
|
// @ts-ignore
|
||||||
req: socket.request,
|
const session = await getSession(socket.request, authOptions)
|
||||||
})
|
|
||||||
if (!session) return next(new Error(status["401"]))
|
if (!session) return next(new Error(status["401"]))
|
||||||
socket.data.user = session.user
|
socket.data.user = session.user
|
||||||
|
|
||||||
|
@ -293,7 +286,4 @@ const SocketHandler = async (
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
res.end()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SocketHandler
|
|
44
leaky-ships/src/routes/game.tsx
Normal file
44
leaky-ships/src/routes/game.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// import { toast } from "react-toastify"
|
||||||
|
import { createEffect } from "solid-js"
|
||||||
|
import { useNavigate } from "solid-start"
|
||||||
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
|
import { useSession } from "~/hooks/useSession"
|
||||||
|
|
||||||
|
export default function Game() {
|
||||||
|
const { payload } = useGameProps()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const session = useSession()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const gameId = payload?.game?.id
|
||||||
|
const path = gameId ? "/game" : "/start"
|
||||||
|
// toast.promise(navigate(path), {
|
||||||
|
// pending: {
|
||||||
|
// render: "Wird weitergeleitet...",
|
||||||
|
// toastId: "pageLoad",
|
||||||
|
// },
|
||||||
|
// success: {
|
||||||
|
// render: gameId
|
||||||
|
// ? "Spiel gefunden!"
|
||||||
|
// : session?.user.id
|
||||||
|
// ? "Kein laufendes Spiel."
|
||||||
|
// : "Kein laufendes Spiel. Bitte anmelden.",
|
||||||
|
// toastId: "pageLoad",
|
||||||
|
// theme: session?.user.id ? "dark" : undefined,
|
||||||
|
// type: gameId ? "success" : "info",
|
||||||
|
// },
|
||||||
|
// error: {
|
||||||
|
// render: "Es ist ein Fehler aufgetreten 🤯",
|
||||||
|
// type: "error",
|
||||||
|
// toastId: "pageLoad",
|
||||||
|
// theme: "colored",
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-full bg-theme">
|
||||||
|
<div class="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
20
leaky-ships/src/routes/gamefield.tsx
Normal file
20
leaky-ships/src/routes/gamefield.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Link, Meta, Title } from "solid-start"
|
||||||
|
import Gamefield from "~/components/Gamefield/Gamefield"
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>Create Next App</Title>
|
||||||
|
<Meta name="description" content="Generated by create next app" />
|
||||||
|
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Link rel="icon" href="/favicon.ico" />
|
||||||
|
<main>
|
||||||
|
<div class="App">
|
||||||
|
<header class="App-header">
|
||||||
|
<Gamefield />
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
20
leaky-ships/src/routes/grid.tsx
Normal file
20
leaky-ships/src/routes/grid.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Link, Meta, Title } from "solid-start"
|
||||||
|
import Grid from "~/components/Grid"
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>Create Next App</Title>
|
||||||
|
<Meta name="description" content="Generated by create next app" />
|
||||||
|
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Link rel="icon" href="/favicon.ico" />
|
||||||
|
<main>
|
||||||
|
<div class="App">
|
||||||
|
<header class="App-header">
|
||||||
|
<Grid />
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
20
leaky-ships/src/routes/grid2.tsx
Normal file
20
leaky-ships/src/routes/grid2.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Link, Meta, Title } from "solid-start"
|
||||||
|
import Grid2 from "~/components/Grid2"
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>Create Next App</Title>
|
||||||
|
<Meta name="description" content="Generated by create next app" />
|
||||||
|
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Link rel="icon" href="/favicon.ico" />
|
||||||
|
<main>
|
||||||
|
<div class="App">
|
||||||
|
<header class="App-header">
|
||||||
|
<Grid2 />
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
30
leaky-ships/src/routes/index.tsx
Normal file
30
leaky-ships/src/routes/index.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { useNavigate } from "solid-start"
|
||||||
|
import BurgerMenu from "~/components/BurgerMenu"
|
||||||
|
import Logo from "~/components/Logo"
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-full bg-theme">
|
||||||
|
<div class="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
|
||||||
|
<Logo />
|
||||||
|
<BurgerMenu />
|
||||||
|
<div class="flex h-36 w-64 items-center justify-center overflow-hidden rounded-xl border-8 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]">
|
||||||
|
<video controls preload="metadata" src="/Regelwerk.mp4" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
class="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("/start")
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
START
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
import BurgerMenu from "@components/BurgerMenu"
|
// import Head from "next/head"
|
||||||
import LobbyFrame from "@components/Lobby/LobbyFrame"
|
|
||||||
import Settings from "@components/Lobby/SettingsFrame/Settings"
|
|
||||||
import Logo from "@components/Logo"
|
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import Head from "next/head"
|
import { createSignal } from "solid-js"
|
||||||
import { useState } from "react"
|
import BurgerMenu from "~/components/BurgerMenu"
|
||||||
|
import LobbyFrame from "~/components/Lobby/LobbyFrame"
|
||||||
|
import Settings from "~/components/Lobby/SettingsFrame/Settings"
|
||||||
|
import Logo from "~/components/Logo"
|
||||||
|
|
||||||
export default function Lobby() {
|
export default function Lobby() {
|
||||||
const [settings, setSettings] = useState(false)
|
const [settings, setSettings] = createSignal(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-theme">
|
<div class="h-full bg-theme">
|
||||||
<Head>
|
{/* <Head>
|
||||||
<title>Lobby</title>
|
<title>Lobby</title>
|
||||||
<meta name="description" content="Generated by create next app" />
|
<meta name="description" content="Generated by create next app" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
@ -22,9 +22,9 @@ export default function Lobby() {
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head> */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
class={classNames(
|
||||||
"mx-auto flex h-full max-w-screen-2xl flex-col items-center justify-evenly",
|
"mx-auto flex h-full max-w-screen-2xl flex-col items-center justify-evenly",
|
||||||
{ "blur-sm": settings },
|
{ "blur-sm": settings },
|
||||||
)}
|
)}
|
||||||
|
@ -32,8 +32,10 @@ export default function Lobby() {
|
||||||
<Logo small={true} />
|
<Logo small={true} />
|
||||||
<LobbyFrame openSettings={() => setSettings(true)} />
|
<LobbyFrame openSettings={() => setSettings(true)} />
|
||||||
</div>
|
</div>
|
||||||
<BurgerMenu blur={settings} />
|
<BurgerMenu blur={settings()} />
|
||||||
{settings ? <Settings closeSettings={() => setSettings(false)} /> : null}
|
{settings() ? (
|
||||||
|
<Settings closeSettings={() => setSettings(false)} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
161
leaky-ships/src/routes/signin.tsx
Normal file
161
leaky-ships/src/routes/signin.tsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import { signIn } from "@auth/solid-start/client"
|
||||||
|
import { faLeftLong } from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { createEffect, createSignal } from "solid-js"
|
||||||
|
import { useNavigate, useSearchParams } from "solid-start"
|
||||||
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
|
import { useSession } from "~/hooks/useSession"
|
||||||
|
|
||||||
|
// import { toast } from "react-toastify"
|
||||||
|
|
||||||
|
type SignInErrorTypes =
|
||||||
|
| "Signin"
|
||||||
|
| "OAuthSignin"
|
||||||
|
| "OAuthCallback"
|
||||||
|
| "OAuthCreateAccount"
|
||||||
|
| "EmailCreateAccount"
|
||||||
|
| "Callback"
|
||||||
|
| "OAuthAccountNotLinked"
|
||||||
|
| "EmailSignin"
|
||||||
|
| "CredentialsSignin"
|
||||||
|
| "SessionRequired"
|
||||||
|
| "default"
|
||||||
|
|
||||||
|
const errors: Record<SignInErrorTypes, string> = {
|
||||||
|
Signin: "Try signing in with a different account.",
|
||||||
|
OAuthSignin: "Try signing in with a different account.",
|
||||||
|
OAuthCallback: "Try signing in with a different account.",
|
||||||
|
OAuthCreateAccount: "Try signing in with a different account.",
|
||||||
|
EmailCreateAccount: "Try signing in with a different account.",
|
||||||
|
Callback: "Try signing in with a different account.",
|
||||||
|
OAuthAccountNotLinked:
|
||||||
|
"To confirm your identity, sign in with the same account you used originally.",
|
||||||
|
EmailSignin: "The e-mail could not be sent.",
|
||||||
|
CredentialsSignin:
|
||||||
|
"Sign in failed. Check the details you provided are correct.",
|
||||||
|
SessionRequired: "Please sign in to access this page.",
|
||||||
|
default: "Unable to sign in.",
|
||||||
|
}
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
const [email, setEmail] = createSignal("")
|
||||||
|
const { state } = useSession()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const errorType = searchParams["error"] as SignInErrorTypes
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!errorType) return
|
||||||
|
// toast.error(errors[errorType] ?? errors.default, { theme: "colored" })
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (state === "ready") navigate("/") //TODO
|
||||||
|
})
|
||||||
|
|
||||||
|
function login(provider: "email" | "azure-ad") {
|
||||||
|
return () => signIn(provider, { email, callbackUrl: "/" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat">
|
||||||
|
<div class="rounded-xl bg-gray-800 bg-opacity-60 px-16 py-10 text-white shadow-lg backdrop-blur-md max-sm:px-8">
|
||||||
|
<div class="mb-8 flex flex-col items-center">
|
||||||
|
<img
|
||||||
|
class="rounded-full shadow-lg"
|
||||||
|
src="/logo512.png"
|
||||||
|
width="150"
|
||||||
|
alt="Avatar"
|
||||||
|
/>
|
||||||
|
<h1 class="mb-2 text-2xl">Leaky Ships</h1>
|
||||||
|
<span
|
||||||
|
class={classNames("text-gray-300", {
|
||||||
|
"rounded-md bg-slate-700 bg-opacity-50 p-1 text-rose-600 ":
|
||||||
|
errorType,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{errorType ? errors[errorType] : "Choose Login Method"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{errorType && <hr class="mb-8 border-gray-400" />}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label for="email" class="mx-2 text-lg">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="my-1 rounded-lg border-2 border-gray-500 bg-slate-800 bg-opacity-60 px-6 py-2 text-center text-inherit placeholder-slate-400 shadow-lg outline-none backdrop-blur-md focus-within:border-blue-500"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="user@example.com"
|
||||||
|
value={email()}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="email-submit"
|
||||||
|
type="submit"
|
||||||
|
onclick={login("email")}
|
||||||
|
class="my-1 rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Sign in with Email
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<hr class="w-full" />
|
||||||
|
<span class="mx-4 my-2">or</span>
|
||||||
|
<hr class="w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2 flex flex-col rounded-lg bg-gradient-to-tr from-[#fff8] via-[#fffd] to-[#fff8] p-4 shadow-lg drop-shadow-md">
|
||||||
|
<a
|
||||||
|
href="https://gbs-grafschaft.de/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/logo-gbs.png"
|
||||||
|
loading="lazy"
|
||||||
|
alt="Gewerbliche Berufsbildende Schulen"
|
||||||
|
class="m-4 mt-2 w-60 justify-center"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
id="microsoft"
|
||||||
|
onClick={login("azure-ad")}
|
||||||
|
class="flex w-full justify-evenly rounded-lg border border-gray-400 bg-slate-100 px-5 py-3 text-black drop-shadow-md duration-300 hover:bg-slate-200"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/Microsoft_icon.svg"
|
||||||
|
loading="lazy"
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
alt="Microsoft_icon"
|
||||||
|
/>
|
||||||
|
<span>Sign in with Microsoft</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{errorType ? (
|
||||||
|
<>
|
||||||
|
<hr class="mt-8 border-gray-400" />
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<button
|
||||||
|
id="back"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
class="mt-10 rounded-lg border-2 border-gray-400 bg-gray-500 bg-opacity-75 px-16 py-2 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:border-blue-600"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLeftLong} />
|
||||||
|
<span class="mx-4 font-bold">Return</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
45
leaky-ships/src/routes/signout.tsx
Normal file
45
leaky-ships/src/routes/signout.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { signOut } from "@auth/solid-start/client"
|
||||||
|
import { createEffect } from "solid-js"
|
||||||
|
import { useNavigate } from "solid-start"
|
||||||
|
import { useSession } from "~/hooks/useSession"
|
||||||
|
|
||||||
|
function Logout() {
|
||||||
|
const { state } = useSession()
|
||||||
|
|
||||||
|
const navigator = useNavigate()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (state === "ready") navigator("/signin") // TODO
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex h-screen w-full items-center justify-center bg-gray-900 bg-[url('/images/wallpaper.jpg')] bg-cover bg-center bg-no-repeat">
|
||||||
|
<div class="rounded-xl bg-gray-800 bg-opacity-50 px-16 py-10 shadow-lg backdrop-blur-md max-sm:px-8">
|
||||||
|
<div class="text-white">
|
||||||
|
<div class="mb-8 flex flex-col items-center">
|
||||||
|
<img
|
||||||
|
class="rounded-full shadow-lg"
|
||||||
|
src="/logo512.png"
|
||||||
|
width="150"
|
||||||
|
alt="Avatar"
|
||||||
|
/>
|
||||||
|
<h1 class="mb-2 text-2xl">Leaky Ships</h1>
|
||||||
|
<span class="text-gray-300">Signout</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-start gap-4">
|
||||||
|
<span>Are you sure you want to sign out?</span>
|
||||||
|
<button
|
||||||
|
id="signout"
|
||||||
|
onClick={() => signOut({ callbackUrl: "/" })}
|
||||||
|
class="rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logout
|
234
leaky-ships/src/routes/start.tsx
Normal file
234
leaky-ships/src/routes/start.tsx
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
import { faEye, faLeftLong } from "@fortawesome/pro-regular-svg-icons"
|
||||||
|
import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { GamePropsSchema } from "~/lib/zodSchemas"
|
||||||
|
// import OtpInput from "react-otp-input"
|
||||||
|
// import { Icons, toast } from "react-toastify"
|
||||||
|
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||||
|
import { useLocation, useNavigate, useSearchParams } from "solid-start"
|
||||||
|
import BurgerMenu from "~/components/BurgerMenu"
|
||||||
|
import FontAwesomeIcon from "~/components/FontAwesomeIcon"
|
||||||
|
import Logo from "~/components/Logo"
|
||||||
|
import OptionButton from "~/components/OptionButton"
|
||||||
|
import { useGameProps } from "~/hooks/useGameProps"
|
||||||
|
import { useSession } from "~/hooks/useSession"
|
||||||
|
|
||||||
|
// export function isAuthenticated(res: Response) {
|
||||||
|
// switch (status[`${res.status}_CLASS`]) {
|
||||||
|
// case status.classes.SUCCESSFUL:
|
||||||
|
// case status.classes.REDIRECTION:
|
||||||
|
// return res.json()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const resStatus = status[`${res.status}_CLASS`]
|
||||||
|
// if (typeof resStatus !== "string") return
|
||||||
|
|
||||||
|
// // toast(status[res.status], {
|
||||||
|
// // position: "top-center",
|
||||||
|
// // type: "info",
|
||||||
|
// // theme: "colored",
|
||||||
|
// // })
|
||||||
|
// }
|
||||||
|
|
||||||
|
const handleConfirmation = () => {
|
||||||
|
const toastId = "confirm"
|
||||||
|
// toast.warn(
|
||||||
|
// <div id="toast-confirm">
|
||||||
|
// <h4>You are already in another round, do you want to:</h4>
|
||||||
|
// <button onClick={() => toast.dismiss(toastId)}>Join</button>
|
||||||
|
// or
|
||||||
|
// <button onClick={() => toast.dismiss(toastId)}>Leave</button>
|
||||||
|
// </div>,
|
||||||
|
// { autoClose: false, toastId },
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Start() {
|
||||||
|
const [otp, setOtp] = createSignal("")
|
||||||
|
const gameProps = useGameProps()
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const session = useSession()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const query = createMemo(() => {
|
||||||
|
switch (searchParams["q"]) {
|
||||||
|
case "join":
|
||||||
|
return { join: true }
|
||||||
|
case "watch":
|
||||||
|
return { watch: true }
|
||||||
|
default:
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const gameFetch = async (pin?: string) => {
|
||||||
|
const gameRequestPromise = fetch(
|
||||||
|
"/api/game/" + (!pin ? "create" : "join"),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ pin }),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// .then(isAuthenticated)
|
||||||
|
.then((game) => GamePropsSchema.parse(game))
|
||||||
|
|
||||||
|
const move = !pin ? "erstellt" : "angefragt"
|
||||||
|
const toastId = "pageLoad"
|
||||||
|
// toast("Raum wird " + move, {
|
||||||
|
// icon: Icons.spinner(),
|
||||||
|
// toastId,
|
||||||
|
// autoClose: false,
|
||||||
|
// hideProgressBar: true,
|
||||||
|
// closeButton: false,
|
||||||
|
// })
|
||||||
|
const res = await gameRequestPromise.catch(() => {
|
||||||
|
// toast.update(toastId, {
|
||||||
|
// render: "Es ist ein Fehler aufgetreten bei der Anfrage 🤯",
|
||||||
|
// type: "error",
|
||||||
|
// icon: Icons.error,
|
||||||
|
// theme: "colored",
|
||||||
|
// autoClose: 5000,
|
||||||
|
// hideProgressBar: false,
|
||||||
|
// closeButton: true,
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
if (!res) return
|
||||||
|
gameProps.full(res)
|
||||||
|
|
||||||
|
// toast.update(toastId, {
|
||||||
|
// render: "Weiterleitung",
|
||||||
|
// })
|
||||||
|
|
||||||
|
navigate("/lobby")
|
||||||
|
// .then(() =>
|
||||||
|
// toast.update(toastId, {
|
||||||
|
// render: "Raum begetreten 👌",
|
||||||
|
// type: "info",
|
||||||
|
// icon: Icons.success,
|
||||||
|
// autoClose: 5000,
|
||||||
|
// hideProgressBar: false,
|
||||||
|
// closeButton: true,
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// .catch(() =>
|
||||||
|
// toast.update(toastId, {
|
||||||
|
// render: "Es ist ein Fehler aufgetreten beim Seiten wechsel 🤯",
|
||||||
|
// type: "error",
|
||||||
|
// icon: Icons.error,
|
||||||
|
// theme: "colored",
|
||||||
|
// autoClose: 5000,
|
||||||
|
// hideProgressBar: false,
|
||||||
|
// closeButton: true,
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (otp().length !== 4) return
|
||||||
|
gameFetch(otp())
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-full bg-theme">
|
||||||
|
<div class="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
|
||||||
|
<Logo />
|
||||||
|
<BurgerMenu />
|
||||||
|
<div class="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">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<button
|
||||||
|
id="back"
|
||||||
|
class="-mt-2 h-14 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("/")
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLeftLong} />
|
||||||
|
</button>
|
||||||
|
{!session.latest?.user?.id && (
|
||||||
|
<button
|
||||||
|
id="login"
|
||||||
|
class="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-orange-500 bg-yellow-500 text-2xl active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-4xl"
|
||||||
|
onClick={() =>
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/signin")
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center gap-6 sm:gap-12">
|
||||||
|
<OptionButton
|
||||||
|
id="Raum erstellen"
|
||||||
|
callback={gameFetch}
|
||||||
|
icon={faPlus}
|
||||||
|
disabled={!session.latest}
|
||||||
|
/>
|
||||||
|
<OptionButton
|
||||||
|
id="Raum beitreten"
|
||||||
|
callback={() =>
|
||||||
|
navigate(
|
||||||
|
location.pathname.concat(
|
||||||
|
"?",
|
||||||
|
new URLSearchParams({ q: "join" }).toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
icon={faUserPlus}
|
||||||
|
disabled={!session.latest}
|
||||||
|
node={
|
||||||
|
query().join && session.latest
|
||||||
|
? // <OtpInput
|
||||||
|
// shouldAutoFocus
|
||||||
|
// containerStyle={{ color: "initial" }}
|
||||||
|
// value={otp}
|
||||||
|
// onChange={setOtp}
|
||||||
|
// numInputs={4}
|
||||||
|
// inputType="number"
|
||||||
|
// inputStyle="inputStyle"
|
||||||
|
// placeholder="0000"
|
||||||
|
// renderSeparator={<span>-</span>}
|
||||||
|
// renderInput={(props) => <input {...props} />}
|
||||||
|
// />
|
||||||
|
null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<OptionButton
|
||||||
|
id="Zuschauen"
|
||||||
|
icon={faEye}
|
||||||
|
callback={() =>
|
||||||
|
navigate(
|
||||||
|
location.pathname.concat(
|
||||||
|
"?",
|
||||||
|
new URLSearchParams({ q: "watch" }).toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
node={
|
||||||
|
query().watch
|
||||||
|
? // <OtpInput
|
||||||
|
// shouldAutoFocus
|
||||||
|
// containerStyle={{ color: "initial" }}
|
||||||
|
// value={otp}
|
||||||
|
// onChange={setOtp}
|
||||||
|
// numInputs={4}
|
||||||
|
// inputType="number"
|
||||||
|
// inputStyle="inputStyle"
|
||||||
|
// placeholder="0000"
|
||||||
|
// renderSeparator={<span>-</span>}
|
||||||
|
// renderInput={(props) => <input {...props} />}
|
||||||
|
// />
|
||||||
|
null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
import prisma from "@lib/prisma"
|
import AzureADProvider from "@auth/core/providers/azure-ad"
|
||||||
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
import EmailProvider from "@auth/core/providers/email"
|
||||||
import { NextApiHandler } from "next"
|
import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||||
import NextAuth, { NextAuthOptions } from "next-auth"
|
import { type SolidAuthConfig } from "@auth/solid-start"
|
||||||
import AzureADProvider from "next-auth/providers/azure-ad"
|
|
||||||
import EmailProvider from "next-auth/providers/email"
|
|
||||||
import {
|
import {
|
||||||
animals,
|
animals,
|
||||||
Config,
|
Config,
|
||||||
NumberDictionary,
|
NumberDictionary,
|
||||||
uniqueNamesGenerator,
|
uniqueNamesGenerator,
|
||||||
} from "unique-names-generator"
|
} from "unique-names-generator"
|
||||||
|
import prisma from "~/lib/prisma"
|
||||||
|
|
||||||
const numberDictionary = NumberDictionary.generate({ min: 0, max: 9999 })
|
const numberDictionary = NumberDictionary.generate({ min: 0, max: 9999 })
|
||||||
const customConfig: Config = {
|
const customConfig: Config = {
|
||||||
|
@ -19,8 +18,9 @@ const customConfig: Config = {
|
||||||
length: 2,
|
length: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: NextAuthOptions = {
|
export const authOptions: SolidAuthConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
|
// @ts-expect-error Types are wrong
|
||||||
EmailProvider({
|
EmailProvider({
|
||||||
server: process.env.EMAIL_SERVER,
|
server: process.env.EMAIL_SERVER,
|
||||||
from: process.env.EMAIL_FROM,
|
from: process.env.EMAIL_FROM,
|
||||||
|
@ -32,7 +32,7 @@ const options: NextAuthOptions = {
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.AUTH_SECRET,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
signIn: ({ user, account }) => {
|
signIn: ({ user, account }) => {
|
||||||
// Custom signIn callback to add username to email provider
|
// Custom signIn callback to add username to email provider
|
||||||
|
@ -56,8 +56,3 @@ const options: NextAuthOptions = {
|
||||||
// newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
|
// newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export { options as authOptions }
|
|
||||||
|
|
||||||
const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options)
|
|
||||||
export default authHandler
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue