commit
84b4025bf9
98 changed files with 9123 additions and 3101 deletions
6
leaky-ships/.gitignore
vendored
6
leaky-ships/.gitignore
vendored
|
@ -1,8 +1,11 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
#logs
|
# logs
|
||||||
/log
|
/log
|
||||||
|
|
||||||
|
# prisma
|
||||||
|
/prisma/migrations
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
@ -29,6 +32,7 @@ yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
|
||||||
"pluginSearchDirs": false
|
|
||||||
}
|
|
28
leaky-ships/components/BurgerMenu.tsx
Normal file
28
leaky-ships/components/BurgerMenu.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
function BurgerMenu({
|
||||||
|
onClick,
|
||||||
|
blur,
|
||||||
|
}: {
|
||||||
|
onClick?: () => void
|
||||||
|
blur?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={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",
|
||||||
|
{ "blur-sm": blur }
|
||||||
|
)}
|
||||||
|
onClick={() => onClick && setTimeout(onClick, 200)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="pixelart h-12 w-12 md:h-16 md:w-16 xl:h-20 xl:w-20"
|
||||||
|
src="/assets/burger-menu.png"
|
||||||
|
alt="Burger Menu"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BurgerMenu
|
|
@ -1,13 +0,0 @@
|
||||||
import Image from "next/image"
|
|
||||||
|
|
||||||
function FogImages() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Image className="fog left" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
|
||||||
<Image className="fog right" src={`/fog/fog2.png`} alt={`fog1.png`} />
|
|
||||||
<Image className="fog middle" src={`/fog/fog4.png`} alt={`fog4.png`} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FogImages
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { CSSProperties } from "react"
|
|
||||||
// import Bluetooth from './Bluetooth'
|
|
||||||
import BorderTiles from "./BorderTiles"
|
|
||||||
import EventBar from "./EventBar"
|
|
||||||
// import FogImages from './FogImages'
|
|
||||||
import HitElems from "./HitElems"
|
|
||||||
import Labeling from "./Labeling"
|
|
||||||
import Ships from "./Ships"
|
|
||||||
import useGameEvent from "../lib/hooks/useGameEvent"
|
|
||||||
import Targets from "./Targets"
|
|
||||||
|
|
||||||
function Gamefield() {
|
|
||||||
const count = 12
|
|
||||||
const { pointersProps, targetsProps, tilesProps, hits } = useGameEvent(count)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="gamefield">
|
|
||||||
{/* <Bluetooth /> */}
|
|
||||||
<div id="game-frame" style={{ "--i": count } as CSSProperties}>
|
|
||||||
{/* Bordes */}
|
|
||||||
<BorderTiles props={tilesProps} />
|
|
||||||
|
|
||||||
{/* Collumn lettes and row numbers */}
|
|
||||||
<Labeling count={count} />
|
|
||||||
|
|
||||||
{/* Ships */}
|
|
||||||
<Ships />
|
|
||||||
|
|
||||||
<HitElems hits={hits} />
|
|
||||||
|
|
||||||
{/* Fog images */}
|
|
||||||
{/* <FogImages /> */}
|
|
||||||
<Targets props={pointersProps} />
|
|
||||||
{/* <span id='dev-debug' style={{gridArea: '1 / 12 / 1 / 15', backgroundColor: 'red', zIndex: 3} as CSSProperties}>Debug</span> */}
|
|
||||||
</div>
|
|
||||||
<EventBar props={targetsProps} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Gamefield
|
|
|
@ -1,23 +1,22 @@
|
||||||
|
import { MouseCursor } from "../../interfaces/frontend"
|
||||||
|
import { count } from "./Gamefield"
|
||||||
|
import { borderCN, cornerCN, fieldIndex } from "@lib/utils/helpers"
|
||||||
import { CSSProperties, Dispatch, SetStateAction } from "react"
|
import { CSSProperties, Dispatch, SetStateAction } from "react"
|
||||||
import { borderCN, cornerCN, fieldIndex } from "../lib/utils/helpers"
|
|
||||||
import { Position, MouseCursor } from "../interfaces/frontend"
|
|
||||||
|
|
||||||
type TilesType = {
|
type TilesType = {
|
||||||
key: number
|
key: number
|
||||||
isGameTile: boolean
|
isGameTile: boolean
|
||||||
classNameString: string
|
className: string
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function BorderTiles({
|
function BorderTiles({
|
||||||
props: { count, settingTarget, setMouseCursor, setLastLeftTile },
|
props: { settingTarget, setMouseCursor },
|
||||||
}: {
|
}: {
|
||||||
props: {
|
props: {
|
||||||
count: number
|
|
||||||
settingTarget: (isGameTile: boolean, x: number, y: number) => void
|
settingTarget: (isGameTile: boolean, x: number, y: number) => void
|
||||||
setMouseCursor: Dispatch<SetStateAction<MouseCursor>>
|
setMouseCursor: Dispatch<SetStateAction<MouseCursor>>
|
||||||
setLastLeftTile: Dispatch<SetStateAction<Position>>
|
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
let tilesProperties: TilesType[] = []
|
let tilesProperties: TilesType[] = []
|
||||||
|
@ -31,10 +30,10 @@ function BorderTiles({
|
||||||
const classNames = ["border-tile"]
|
const classNames = ["border-tile"]
|
||||||
if (borderType) classNames.push("edge", borderType)
|
if (borderType) classNames.push("edge", borderType)
|
||||||
if (isGameTile) classNames.push("game-tile")
|
if (isGameTile) classNames.push("game-tile")
|
||||||
const classNameString = classNames.join(" ")
|
const className = classNames.join(" ")
|
||||||
tilesProperties.push({
|
tilesProperties.push({
|
||||||
key,
|
key,
|
||||||
classNameString,
|
className,
|
||||||
isGameTile,
|
isGameTile,
|
||||||
x: x + 1,
|
x: x + 1,
|
||||||
y: y + 1,
|
y: y + 1,
|
||||||
|
@ -44,17 +43,16 @@ function BorderTiles({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{tilesProperties.map(({ key, classNameString, isGameTile, x, y }) => {
|
{tilesProperties.map(({ key, className, isGameTile, x, y }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className={classNameString}
|
className={className}
|
||||||
style={{ "--x": x, "--y": y } as CSSProperties}
|
style={{ "--x": x, "--y": y } as CSSProperties}
|
||||||
onClick={() => settingTarget(isGameTile, x, y)}
|
onClick={() => settingTarget(isGameTile, x, y)}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
setMouseCursor({ x, y, shouldShow: isGameTile })
|
setMouseCursor({ x, y, shouldShow: isGameTile })
|
||||||
}
|
}
|
||||||
onMouseLeave={() => setLastLeftTile({ x, y })}
|
|
||||||
></div>
|
></div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { Dispatch, SetStateAction } from "react"
|
import { Items, Target } from "../../interfaces/frontend"
|
||||||
import { Items, Target } from "../interfaces/frontend"
|
|
||||||
import Item from "./Item"
|
import Item from "./Item"
|
||||||
|
import React, { Dispatch, SetStateAction } from "react"
|
||||||
|
|
||||||
function EventBar({
|
function EventBar({
|
||||||
props: { setMode, setTarget },
|
props: { setMode, setTarget },
|
||||||
|
@ -14,7 +14,7 @@ function EventBar({
|
||||||
{ icon: "burger-menu", text: "Menu" },
|
{ icon: "burger-menu", text: "Menu" },
|
||||||
{ icon: "radar", text: "Radar scan", mode: 0, amount: 1 },
|
{ icon: "radar", text: "Radar scan", mode: 0, amount: 1 },
|
||||||
{ icon: "torpedo", text: "Fire torpedo", mode: 1, amount: 1 },
|
{ icon: "torpedo", text: "Fire torpedo", mode: 1, amount: 1 },
|
||||||
{ icon: "scope", text: "Fire missile", mode: 2 },
|
{ icon: "scope", text: "Fire missile", mode: 3 },
|
||||||
{ icon: "gear", text: "Settings" },
|
{ icon: "gear", text: "Settings" },
|
||||||
]
|
]
|
||||||
return (
|
return (
|
13
leaky-ships/components/Gamefield/FogImages.tsx
Normal file
13
leaky-ships/components/Gamefield/FogImages.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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
|
139
leaky-ships/components/Gamefield/Gamefield.tsx
Normal file
139
leaky-ships/components/Gamefield/Gamefield.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import { Draw, Hit, MouseCursor, Target } from "../../interfaces/frontend"
|
||||||
|
// import Bluetooth from "./Bluetooth"
|
||||||
|
// import FogImages from "./FogImages"
|
||||||
|
import Labeling from "./Labeling"
|
||||||
|
import Ships from "./Ships"
|
||||||
|
import BorderTiles from "@components/Gamefield/BorderTiles"
|
||||||
|
import EventBar from "@components/Gamefield/EventBar"
|
||||||
|
import HitElems from "@components/Gamefield/HitElems"
|
||||||
|
import Targets from "@components/Gamefield/Targets"
|
||||||
|
import { useDraw } from "@hooks/useDraw"
|
||||||
|
import {
|
||||||
|
hitReducer,
|
||||||
|
initlialTarget,
|
||||||
|
initlialTargetPreview,
|
||||||
|
initlialMouseCursor,
|
||||||
|
overlapsWithAnyBorder,
|
||||||
|
isAlreadyHit,
|
||||||
|
targetList,
|
||||||
|
} from "@lib/utils/helpers"
|
||||||
|
import { CSSProperties, useCallback } from "react"
|
||||||
|
import { useEffect, useReducer, useState } from "react"
|
||||||
|
|
||||||
|
export const count = 12
|
||||||
|
|
||||||
|
function Gamefield() {
|
||||||
|
const [target, setTarget] = useState<Target>(initlialTarget)
|
||||||
|
const [targetPreview, setTargetPreview] = useState<Target>(
|
||||||
|
initlialTargetPreview
|
||||||
|
)
|
||||||
|
const [mouseCursor, setMouseCursor] =
|
||||||
|
useState<MouseCursor>(initlialMouseCursor)
|
||||||
|
const [hits, DispatchHits] = useReducer(hitReducer, [] as Hit[])
|
||||||
|
const [mode, setMode] = useState(0)
|
||||||
|
|
||||||
|
const settingTarget = useCallback(
|
||||||
|
(isGameTile: boolean, x: number, y: number) => {
|
||||||
|
const list = targetList(targetPreview, mode)
|
||||||
|
if (
|
||||||
|
!isGameTile ||
|
||||||
|
!list.filter(({ x, y }) => !isAlreadyHit(x, y, hits)).length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if (target.show && target.x == x && target.y == y) {
|
||||||
|
DispatchHits({
|
||||||
|
type: "fireMissile",
|
||||||
|
payload: list.map(({ x, y }) => ({
|
||||||
|
hit: isAlreadyHit(x, y, hits),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
setTarget((t) => ({ ...t, show: false }))
|
||||||
|
} else if (!overlapsWithAnyBorder(targetPreview, mode))
|
||||||
|
setTarget({ show: true, x, y })
|
||||||
|
},
|
||||||
|
[hits, mode, target, targetPreview]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { x, y, show } = target
|
||||||
|
const { shouldShow, ...position } = mouseCursor
|
||||||
|
if (!shouldShow || overlapsWithAnyBorder(position, mode))
|
||||||
|
setTargetPreview((e) => ({ ...e, show: false }))
|
||||||
|
else {
|
||||||
|
setTargetPreview({
|
||||||
|
...position,
|
||||||
|
show: !show || x !== position.x || y !== position.y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [mode, mouseCursor, target])
|
||||||
|
|
||||||
|
const [color, setColor] = useState<string>("#f00")
|
||||||
|
const [disable, setDisable] = useState(false)
|
||||||
|
const { canvasRef, onMouseDown, clear } = useDraw(drawLine)
|
||||||
|
|
||||||
|
function drawLine({ prevPoint, currentPoint, ctx }: Draw) {
|
||||||
|
const { x: currX, y: currY } = currentPoint
|
||||||
|
const lineColor = color
|
||||||
|
const lineWidth = 5
|
||||||
|
|
||||||
|
let startPoint = prevPoint ?? currentPoint
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.lineWidth = lineWidth
|
||||||
|
ctx.strokeStyle = lineColor
|
||||||
|
ctx.moveTo(startPoint.x, startPoint.y)
|
||||||
|
ctx.lineTo(currX, currY)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.fillStyle = lineColor
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(startPoint.x, startPoint.y, 2, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="gamefield">
|
||||||
|
{/* <Bluetooth /> */}
|
||||||
|
<div
|
||||||
|
id="game-frame"
|
||||||
|
style={{ "--i": count } as CSSProperties}
|
||||||
|
onMouseLeave={() =>
|
||||||
|
setMouseCursor((e) => ({ ...e, shouldShow: false }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Bordes */}
|
||||||
|
<BorderTiles props={{ settingTarget, setMouseCursor }} />
|
||||||
|
|
||||||
|
{/* Collumn lettes and row numbers */}
|
||||||
|
<Labeling count={count} />
|
||||||
|
|
||||||
|
{/* Ships */}
|
||||||
|
<Ships />
|
||||||
|
<HitElems hits={hits} />
|
||||||
|
|
||||||
|
{/* Fog images */}
|
||||||
|
{/* <FogImages /> */}
|
||||||
|
|
||||||
|
<Targets props={{ target, targetPreview, mode, hits }} />
|
||||||
|
{/* <span id='dev-debug' style={{gridArea: '1 / 12 / 1 / 15', backgroundColor: 'red', zIndex: 3} as CSSProperties}>Debug</span> */}
|
||||||
|
<canvas
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
pointerEvents: !disable ? "auto" : "none",
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
ref={canvasRef}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
width="648"
|
||||||
|
height="648"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setDisable((e) => !e)}>toggle disable</button>
|
||||||
|
<button onClick={clear}>Clear</button>
|
||||||
|
<EventBar props={{ setMode, setTarget }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gamefield
|
|
@ -1,18 +1,20 @@
|
||||||
|
import { Target, TargetList } from "../../interfaces/frontend"
|
||||||
import { faCrosshairs } from "@fortawesome/pro-solid-svg-icons"
|
import { faCrosshairs } from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { CSSProperties } from "react"
|
|
||||||
import classNames from "classnames"
|
|
||||||
import { faRadar } from "@fortawesome/pro-thin-svg-icons"
|
import { faRadar } from "@fortawesome/pro-thin-svg-icons"
|
||||||
import { Target, TargetList } from "../interfaces/frontend"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { CSSProperties } from "react"
|
||||||
|
|
||||||
export interface PointerProps extends Target, TargetList {
|
export interface PointerProps extends Target, TargetList {
|
||||||
imply: boolean
|
imply: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function GamefieldPointer({
|
function GamefieldPointer({
|
||||||
props: { preview, x, y, show, type, edges, imply },
|
props: { x, y, show, type, edges, imply },
|
||||||
|
preview,
|
||||||
}: {
|
}: {
|
||||||
props: PointerProps
|
props: PointerProps
|
||||||
|
preview?: boolean
|
||||||
}) {
|
}) {
|
||||||
const isRadar = type === "radar"
|
const isRadar = type === "radar"
|
||||||
const style = !(isRadar && !edges.filter((s) => s).length)
|
const style = !(isRadar && !edges.filter((s) => s).length)
|
||||||
|
@ -20,15 +22,11 @@ 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(
|
className={classNames("hit-svg", "target", type, ...edges, {
|
||||||
"hit-svg",
|
preview: preview,
|
||||||
{ preview: preview },
|
show: show,
|
||||||
"target",
|
imply: imply,
|
||||||
type,
|
})}
|
||||||
{ show: show },
|
|
||||||
...edges,
|
|
||||||
{ imply: imply }
|
|
||||||
)}
|
|
||||||
style={style as CSSProperties}
|
style={style as CSSProperties}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={!isRadar ? faCrosshairs : faRadar} />
|
<FontAwesomeIcon icon={!isRadar ? faCrosshairs : faRadar} />
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { Hit } from "../../interfaces/frontend"
|
||||||
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 "@fortawesome/react-fontawesome"
|
||||||
import { CSSProperties } from "react"
|
import { CSSProperties } from "react"
|
||||||
import { Hit } from "../interfaces/frontend"
|
|
||||||
|
|
||||||
function HitElems({ hits }: { hits: Hit[] }) {
|
function HitElems({ hits }: { hits: Hit[] }) {
|
||||||
return (
|
return (
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { Field } from "../../interfaces/frontend"
|
||||||
|
import { fieldIndex } from "@lib/utils/helpers"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { CSSProperties } from "react"
|
import { CSSProperties } from "react"
|
||||||
import { fieldIndex } from "../lib/utils/helpers"
|
|
||||||
import { Field } from "../interfaces/frontend"
|
|
||||||
|
|
||||||
function Labeling({ count }: { count: number }) {
|
function Labeling({ count }: { count: number }) {
|
||||||
let elems: (Field & {
|
let elems: (Field & {
|
30
leaky-ships/components/Gamefield/Targets.tsx
Normal file
30
leaky-ships/components/Gamefield/Targets.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { Hit, Target } from "../../interfaces/frontend"
|
||||||
|
import GamefieldPointer from "./GamefieldPointer"
|
||||||
|
import { composeTargetTiles } from "@lib/utils/helpers"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
function Targets({
|
||||||
|
props: { target, targetPreview, mode, hits },
|
||||||
|
}: {
|
||||||
|
props: {
|
||||||
|
target: Target
|
||||||
|
targetPreview: Target
|
||||||
|
mode: number
|
||||||
|
hits: Hit[]
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{[
|
||||||
|
...composeTargetTiles(target, mode, hits).map((props, i) => (
|
||||||
|
<GamefieldPointer key={"t" + i} props={props} />
|
||||||
|
)),
|
||||||
|
...composeTargetTiles(targetPreview, mode, hits).map((props, i) => (
|
||||||
|
<GamefieldPointer key={"p" + i} props={props} preview />
|
||||||
|
)),
|
||||||
|
]}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Targets
|
46
leaky-ships/components/Lobby/Button.tsx
Normal file
46
leaky-ships/components/Lobby/Button.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
import React from "react"
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
latching,
|
||||||
|
isLatched,
|
||||||
|
}: {
|
||||||
|
type: "red" | "orange" | "green" | "gray"
|
||||||
|
disabled?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
children: ReactNode
|
||||||
|
latching?: boolean
|
||||||
|
isLatched?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={disabled}
|
||||||
|
className={classNames(
|
||||||
|
"font-farro rounded-xl px-8 py-4 text-5xl font-medium duration-100",
|
||||||
|
disabled
|
||||||
|
? "border-4 border-dashed"
|
||||||
|
: latching
|
||||||
|
? isLatched
|
||||||
|
? "mx-1 my-0.5 border-t-4"
|
||||||
|
: "mx-1 my-0.5 border-b-4"
|
||||||
|
: "mx-1 my-0.5 border-b-4 active:border-b-0 active:border-t-4",
|
||||||
|
{
|
||||||
|
"border-red-600 bg-red-500": type === "red",
|
||||||
|
"border-orange-400 bg-warn": type === "orange",
|
||||||
|
"border-green-600 bg-green-500": type === "green",
|
||||||
|
"border-gray-600 bg-gray-500": type === "gray",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button
|
27
leaky-ships/components/Lobby/Icon.tsx
Normal file
27
leaky-ships/components/Lobby/Icon.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
|
||||||
|
function Icon({
|
||||||
|
src,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
src: string
|
||||||
|
children: ReactNode
|
||||||
|
onClick?: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="mx-4 mt-4 flex flex-col items-center border-none"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="pixelart mb-1 box-content w-16 rounded-xl bg-white p-1"
|
||||||
|
src={"/assets/" + src}
|
||||||
|
alt={src}
|
||||||
|
/>
|
||||||
|
<span className="font-semibold">{children}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Icon
|
130
leaky-ships/components/Lobby/LobbyFrame.tsx
Normal file
130
leaky-ships/components/Lobby/LobbyFrame.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import Button from "./Button"
|
||||||
|
import Icon from "./Icon"
|
||||||
|
import Player from "./Player"
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 >= 1) return
|
||||||
|
router.push("/gamefield")
|
||||||
|
}, [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])
|
||||||
|
|
||||||
|
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>{"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 () => {
|
||||||
|
await router.push("/")
|
||||||
|
reset()
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>LEAVE</span>
|
||||||
|
<FontAwesomeIcon icon={faRightFromBracket} className="ml-4 w-12" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LobbyFrame
|
128
leaky-ships/components/Lobby/Player.tsx
Normal file
128
leaky-ships/components/Lobby/Player.tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import Button from "./Button"
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faHandPointer,
|
||||||
|
faHourglass1,
|
||||||
|
faHourglass2,
|
||||||
|
faHourglass3,
|
||||||
|
faHourglassClock,
|
||||||
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
|
||||||
|
import { useGameProps } from "@hooks/useGameProps"
|
||||||
|
import { socket } from "@lib/socket"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { CSSProperties, useEffect, useMemo, useState } from "react"
|
||||||
|
|
||||||
|
function HourGlass() {
|
||||||
|
const [count, setCount] = useState(3)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => setCount((e) => (e + 1) % 4), 1000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const icon = useMemo(() => {
|
||||||
|
switch (count) {
|
||||||
|
case 0:
|
||||||
|
return faHourglass3
|
||||||
|
case 1:
|
||||||
|
return faHourglass1
|
||||||
|
case 2:
|
||||||
|
return faHourglass2
|
||||||
|
case 3:
|
||||||
|
return faHourglass3
|
||||||
|
default:
|
||||||
|
return faHourglassClock
|
||||||
|
}
|
||||||
|
}, [count])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FontAwesomeIcon icon={icon} className="ml-4 w-12" spin={count === 0} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Player({
|
||||||
|
src,
|
||||||
|
i,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
src: string
|
||||||
|
i: number
|
||||||
|
userId?: string
|
||||||
|
}) {
|
||||||
|
const { payload, userStates, setIsReady } = useGameProps()
|
||||||
|
const player = useMemo(() => payload?.users[i], [i, payload?.users])
|
||||||
|
const { isReady, isConnected } = useMemo(() => userStates[i], [i, userStates])
|
||||||
|
const primary = useMemo(
|
||||||
|
() => userId && userId === payload?.users[i]?.id,
|
||||||
|
[i, payload?.users, userId]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-96 flex-col items-center gap-4 p-4">
|
||||||
|
<p
|
||||||
|
className={classNames(
|
||||||
|
"font-farro w-max text-5xl",
|
||||||
|
primary ? "font-semibold" : "font-normal"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{player?.name ?? "Spieler " + (player?.index === 2 ? "2" : "1")}
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<img className="pixelart w-64" src={"/assets/" + src} alt={src} />
|
||||||
|
{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">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="h-full w-full text-warn"
|
||||||
|
icon={faCaretDown}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type={isConnected ? (isReady ? "green" : "orange") : "gray"}
|
||||||
|
latching
|
||||||
|
isLatched={!!isReady}
|
||||||
|
onClick={() => {
|
||||||
|
if (!player) return
|
||||||
|
console.log(i, !isReady)
|
||||||
|
setIsReady({
|
||||||
|
i,
|
||||||
|
isReady: !isReady,
|
||||||
|
})
|
||||||
|
socket.emit("isReady", !isReady)
|
||||||
|
}}
|
||||||
|
disabled={!primary}
|
||||||
|
>
|
||||||
|
Ready
|
||||||
|
{isReady && isConnected ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="ml-4 w-12" />
|
||||||
|
) : primary ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faHandPointer}
|
||||||
|
className="ml-4 w-12"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--fa-bounce-start-scale-x": 1.05,
|
||||||
|
"--fa-bounce-start-scale-y": 0.95,
|
||||||
|
"--fa-bounce-jump-scale-x": 0.95,
|
||||||
|
"--fa-bounce-jump-scale-y": 1.05,
|
||||||
|
"--fa-bounce-land-scale-x": 1.025,
|
||||||
|
"--fa-bounce-land-scale-y": 0.975,
|
||||||
|
"--fa-bounce-height": "-0.125em",
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
bounce
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HourGlass />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Player
|
61
leaky-ships/components/Lobby/SettingsFrame/Setting.tsx
Normal file
61
leaky-ships/components/Lobby/SettingsFrame/Setting.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
faToggleLargeOff,
|
||||||
|
faToggleLargeOn,
|
||||||
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import { useGameProps } from "@hooks/useGameProps"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { ReactNode, useMemo } from "react"
|
||||||
|
|
||||||
|
type GameSettingKeys =
|
||||||
|
| "allowSpectators"
|
||||||
|
| "allowSpecials"
|
||||||
|
| "allowChat"
|
||||||
|
| "allowMarkDraw"
|
||||||
|
|
||||||
|
export type GameSettings = { [key in GameSettingKeys]?: boolean }
|
||||||
|
|
||||||
|
function Setting({
|
||||||
|
children,
|
||||||
|
props: { prop, gameSetting },
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
props: { prop: GameSettingKeys; gameSetting: (payload: GameSettings) => void }
|
||||||
|
}) {
|
||||||
|
const { payload } = useGameProps()
|
||||||
|
const state = useMemo(() => payload?.game?.[prop], [payload?.game, prop])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="flex items-center justify-between" htmlFor={prop}>
|
||||||
|
<span className="col-span-2 w-96 select-none text-5xl text-white drop-shadow-md">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className={classNames(
|
||||||
|
"text-md mx-auto rounded-full px-4 drop-shadow-md transition-all",
|
||||||
|
state ? "text-blue-500" : "text-gray-800",
|
||||||
|
{
|
||||||
|
"bg-gray-300 ": state,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
size="3x"
|
||||||
|
icon={state ? faToggleLargeOn : faToggleLargeOff}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="bg-none"
|
||||||
|
checked={state}
|
||||||
|
type="checkbox"
|
||||||
|
id={prop}
|
||||||
|
onChange={() => {
|
||||||
|
const payload = {
|
||||||
|
[prop]: !state,
|
||||||
|
}
|
||||||
|
gameSetting(payload)
|
||||||
|
}}
|
||||||
|
hidden={true}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Setting
|
85
leaky-ships/components/Lobby/SettingsFrame/Settings.tsx
Normal file
85
leaky-ships/components/Lobby/SettingsFrame/Settings.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import Setting, { GameSettings } from "./Setting"
|
||||||
|
import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
|
||||||
|
import { faXmark } from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import { useGameProps } from "@hooks/useGameProps"
|
||||||
|
import { socket } from "@lib/socket"
|
||||||
|
import { useCallback } from "react"
|
||||||
|
|
||||||
|
function Settings({ closeSettings }: { closeSettings: () => void }) {
|
||||||
|
const { setSetting, full } = useGameProps()
|
||||||
|
|
||||||
|
const gameSetting = useCallback(
|
||||||
|
(payload: GameSettings) => {
|
||||||
|
const hash = setSetting(payload)
|
||||||
|
socket.emit("gameSetting", payload, (newHash) => {
|
||||||
|
if (newHash === hash) return
|
||||||
|
console.log("hash", hash, newHash)
|
||||||
|
socket.emit("update", full)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[full, setSetting]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/40">
|
||||||
|
<div className="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 className="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">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
className="right-6 top-6 ml-auto h-14 w-14"
|
||||||
|
onClick={closeSettings}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="h-full w-full text-gray-800 drop-shadow-md"
|
||||||
|
size="3x"
|
||||||
|
icon={faXmark}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 rounded-xl bg-zinc-600 p-8">
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<button
|
||||||
|
className="right-12 top-8 h-14 w-14"
|
||||||
|
onClick={() => {
|
||||||
|
const payload = {
|
||||||
|
allowSpectators: true,
|
||||||
|
allowSpecials: true,
|
||||||
|
allowChat: true,
|
||||||
|
allowMarkDraw: true,
|
||||||
|
}
|
||||||
|
gameSetting(payload)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="h-full w-full text-gray-800 drop-shadow-md"
|
||||||
|
size="3x"
|
||||||
|
icon={faRotateLeft}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<Setting props={{ gameSetting, prop: "allowSpectators" }}>
|
||||||
|
Erlaube Zuschauer
|
||||||
|
</Setting>
|
||||||
|
<Setting props={{ gameSetting, prop: "allowSpecials" }}>
|
||||||
|
Erlaube spezial Items
|
||||||
|
</Setting>
|
||||||
|
<Setting props={{ gameSetting, prop: "allowChat" }}>
|
||||||
|
Erlaube den Chat
|
||||||
|
</Setting>
|
||||||
|
<Setting props={{ gameSetting, prop: "allowMarkDraw" }}>
|
||||||
|
Erlaube zeichen/makieren
|
||||||
|
</Setting>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
84
leaky-ships/components/Logo.tsx
Normal file
84
leaky-ships/components/Logo.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
function Logo({ small }: { small?: boolean }) {
|
||||||
|
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">
|
||||||
|
<h1
|
||||||
|
className={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",
|
||||||
|
{ "xl:gap-6 xl:py-2 xl:text-6xl": !small }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Leaky</span>
|
||||||
|
<span>Ships</span>
|
||||||
|
</h1>
|
||||||
|
<Screws small={small} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Screws({ small }: { small?: boolean }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Screw
|
||||||
|
small={small}
|
||||||
|
orientation={classNames("top-1 left-1 sm:top-2 sm:left-2", {
|
||||||
|
"xl:top-4 xl:left-4": !small,
|
||||||
|
})}
|
||||||
|
rotation="rotate-[135deg]"
|
||||||
|
/>
|
||||||
|
<Screw
|
||||||
|
small={small}
|
||||||
|
orientation={classNames("top-1 right-1 sm:top-2 sm:right-2", {
|
||||||
|
"xl:top-4 xl:right-4": !small,
|
||||||
|
})}
|
||||||
|
rotation="rotate-[5deg]"
|
||||||
|
/>
|
||||||
|
<Screw
|
||||||
|
small={small}
|
||||||
|
orientation={classNames("bottom-1 right-1 sm:bottom-2 sm:right-2", {
|
||||||
|
"xl:bottom-4 xl:right-4": !small,
|
||||||
|
})}
|
||||||
|
rotation="rotate-[150deg]"
|
||||||
|
/>
|
||||||
|
<Screw
|
||||||
|
small={small}
|
||||||
|
orientation={classNames("bottom-1 left-1 sm:bottom-2 sm:left-2", {
|
||||||
|
"xl:bottom-4 xl:left-4": !small,
|
||||||
|
})}
|
||||||
|
rotation="rotate-[20deg]"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Screw({
|
||||||
|
orientation,
|
||||||
|
rotation,
|
||||||
|
small,
|
||||||
|
}: {
|
||||||
|
orientation: string
|
||||||
|
rotation: string
|
||||||
|
small?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={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",
|
||||||
|
{ "xl:h-8 xl:w-8": !small },
|
||||||
|
orientation
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<hr
|
||||||
|
className={classNames(
|
||||||
|
"color w-full border-neutral-500 sm:border-t-2",
|
||||||
|
{ "xl:border-t-4": !small },
|
||||||
|
rotation
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logo
|
40
leaky-ships/components/OptionButton.tsx
Normal file
40
leaky-ships/components/OptionButton.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconProps,
|
||||||
|
} from "@fortawesome/react-fontawesome"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
|
||||||
|
function OptionButton({
|
||||||
|
icon,
|
||||||
|
action,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
icon: FontAwesomeIconProps["icon"]
|
||||||
|
action?: () => void
|
||||||
|
children: ReactNode
|
||||||
|
disabled?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={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",
|
||||||
|
!disabled
|
||||||
|
? "border-b-4 border-shield-gray bg-voidDark active:border-b-0 active:border-t-4"
|
||||||
|
: "border-4 border-dashed border-slate-600 bg-red-950"
|
||||||
|
)}
|
||||||
|
onClick={() => action && setTimeout(action, 200)}
|
||||||
|
disabled={disabled}
|
||||||
|
title={!disabled ? "" : "Please login"}
|
||||||
|
>
|
||||||
|
<span className="mx-auto">{children}</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="ml-2 w-10 text-xl sm:ml-12 sm:text-4xl"
|
||||||
|
icon={icon}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OptionButton
|
|
@ -1,38 +0,0 @@
|
||||||
import { useEffect } from "react"
|
|
||||||
import { io } from "socket.io-client"
|
|
||||||
|
|
||||||
function SocketIO() {
|
|
||||||
useEffect(() => {
|
|
||||||
socketInitializer()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const socketInitializer = async () => {
|
|
||||||
await fetch("/api/ws")
|
|
||||||
|
|
||||||
const socket = io()
|
|
||||||
socket.on("test2", (warst) => {
|
|
||||||
console.log("Test2:", warst, socket.id)
|
|
||||||
})
|
|
||||||
socket.on("connect", () => {
|
|
||||||
console.log(socket.connected) // true
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.emit("test", "warst")
|
|
||||||
socket.emit("test", "tsra")
|
|
||||||
socket.emit("test", "1234")
|
|
||||||
// socket.disconnect()
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on("test", () => {
|
|
||||||
console.log("Got test1234") // false
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
console.log(socket.connected) // false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>SocketIO</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SocketIO
|
|
|
@ -1,28 +0,0 @@
|
||||||
import React from "react"
|
|
||||||
import { Target } from "../interfaces/frontend"
|
|
||||||
import GamefieldPointer, { PointerProps } from "./GamefieldPointer"
|
|
||||||
|
|
||||||
function Targets({
|
|
||||||
props: { composeTargetTiles, target, targetPreview },
|
|
||||||
}: {
|
|
||||||
props: {
|
|
||||||
composeTargetTiles: (target: Target) => PointerProps[]
|
|
||||||
target: Target
|
|
||||||
targetPreview: Target
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{[
|
|
||||||
...composeTargetTiles(target).map((props, i) => (
|
|
||||||
<GamefieldPointer key={"t" + i} props={props} />
|
|
||||||
)),
|
|
||||||
...composeTargetTiles(targetPreview).map((props, i) => (
|
|
||||||
<GamefieldPointer key={"p" + i} props={props} />
|
|
||||||
)),
|
|
||||||
]}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Targets
|
|
13
leaky-ships/components/profileImg.tsx
Normal file
13
leaky-ships/components/profileImg.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
function profileImg(src: string) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={{ transform: "scale(1.5)", borderRadius: "100%" }}
|
||||||
|
src={src}
|
||||||
|
alt="profile picture"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default profileImg
|
4
leaky-ships/global.d.ts
vendored
4
leaky-ships/global.d.ts
vendored
|
@ -1,3 +1,5 @@
|
||||||
declare module globalThis {
|
import "@total-typescript/ts-reset"
|
||||||
|
|
||||||
|
declare global {
|
||||||
var prismaClient: PrismaClient
|
var prismaClient: PrismaClient
|
||||||
}
|
}
|
||||||
|
|
64
leaky-ships/hooks/useDraw.ts
Normal file
64
leaky-ships/hooks/useDraw.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { Draw, Point } from "../interfaces/frontend"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
export const useDraw = (
|
||||||
|
onDraw: ({ ctx, currentPoint, prevPoint }: Draw) => void
|
||||||
|
) => {
|
||||||
|
const [mouseDown, setMouseDown] = useState(false)
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const prevPoint = useRef<null | Point>(null)
|
||||||
|
|
||||||
|
const onMouseDown = () => setMouseDown(true)
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d")
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (!mouseDown) return
|
||||||
|
const currentPoint = computePointInCanvas(e)
|
||||||
|
|
||||||
|
const ctx = canvasRef.current?.getContext("2d")
|
||||||
|
if (!ctx || !currentPoint) return
|
||||||
|
|
||||||
|
onDraw({ ctx, currentPoint, prevPoint: prevPoint.current })
|
||||||
|
prevPoint.current = currentPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
const computePointInCanvas = (e: MouseEvent) => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const y = e.clientY - rect.top
|
||||||
|
|
||||||
|
return { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
const mouseUpHandler = () => {
|
||||||
|
setMouseDown(false)
|
||||||
|
prevPoint.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
canvasRef.current?.addEventListener("mousemove", handler)
|
||||||
|
window.addEventListener("mouseup", mouseUpHandler)
|
||||||
|
|
||||||
|
// Remove event listeners
|
||||||
|
return () => {
|
||||||
|
canvasRef.current?.removeEventListener("mousemove", handler)
|
||||||
|
window.removeEventListener("mouseup", mouseUpHandler)
|
||||||
|
}
|
||||||
|
}, [onDraw])
|
||||||
|
|
||||||
|
return { canvasRef, onMouseDown, clear }
|
||||||
|
}
|
138
leaky-ships/hooks/useGameProps.ts
Normal file
138
leaky-ships/hooks/useGameProps.ts
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
|
||||||
|
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
|
||||||
|
import { socket } from "@lib/socket"
|
||||||
|
import {
|
||||||
|
GamePropsSchema,
|
||||||
|
optionalGamePropsSchema,
|
||||||
|
PlayerSchema,
|
||||||
|
} from "@lib/zodSchemas"
|
||||||
|
import { produce } from "immer"
|
||||||
|
import { toast } from "react-toastify"
|
||||||
|
import { create } from "zustand"
|
||||||
|
import { devtools } from "zustand/middleware"
|
||||||
|
|
||||||
|
const initialState: optionalGamePropsSchema & {
|
||||||
|
userStates: {
|
||||||
|
isReady: boolean
|
||||||
|
isConnected: boolean
|
||||||
|
}[]
|
||||||
|
} = {
|
||||||
|
payload: null,
|
||||||
|
hash: null,
|
||||||
|
userStates: Array.from(Array(2), () => ({
|
||||||
|
isReady: false,
|
||||||
|
isConnected: false,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
export type State = typeof initialState
|
||||||
|
|
||||||
|
export type Action = {
|
||||||
|
setSetting: (settings: GameSettings) => string | null
|
||||||
|
setPlayer: (payload: { users: PlayerSchema[] }) => string | null
|
||||||
|
full: (newProps: GamePropsSchema) => void
|
||||||
|
leave: (cb: () => void) => void
|
||||||
|
setIsReady: (payload: { i: number; isReady: boolean }) => void
|
||||||
|
setIsConnected: (payload: { i: number; isConnected: boolean }) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGameProps = create<State & Action>()(
|
||||||
|
devtools(
|
||||||
|
(set) => ({
|
||||||
|
...initialState,
|
||||||
|
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) => {
|
||||||
|
const payload = JSON.stringify(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
|
||||||
|
})
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
175
leaky-ships/hooks/useSocket.ts
Normal file
175
leaky-ships/hooks/useSocket.ts
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import { useGameProps } from "./useGameProps"
|
||||||
|
import { socket } from "@lib/socket"
|
||||||
|
import status from "http-status"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { toast } from "react-toastify"
|
||||||
|
|
||||||
|
/** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
|
||||||
|
function useSocket() {
|
||||||
|
const [isConnectedState, setIsConnectedState] = useState(false)
|
||||||
|
const {
|
||||||
|
payload,
|
||||||
|
userStates,
|
||||||
|
setPlayer,
|
||||||
|
setSetting,
|
||||||
|
full,
|
||||||
|
setIsReady,
|
||||||
|
setIsConnected,
|
||||||
|
hash: stateHash,
|
||||||
|
} = useGameProps()
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { i, isIndex } = useMemo(() => {
|
||||||
|
const i = payload?.users.findIndex((user) => session?.user?.id === user?.id)
|
||||||
|
const isIndex = !(i === undefined || i < 0)
|
||||||
|
if (!isIndex) return { i: undefined, isIndex }
|
||||||
|
return { i, isIndex }
|
||||||
|
}, [payload?.users, session?.user?.id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isIndex) return
|
||||||
|
setIsConnected({
|
||||||
|
i,
|
||||||
|
isConnected: isConnectedState,
|
||||||
|
})
|
||||||
|
}, [i, isConnectedState, isIndex, setIsConnected])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session?.user.id) return
|
||||||
|
socket.connect()
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("connected")
|
||||||
|
toast.dismiss("connect_error")
|
||||||
|
setIsConnectedState(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("connect_error", (error) => {
|
||||||
|
console.log("Connection error:", error.message)
|
||||||
|
if (error.message === status["403"]) router.push("/")
|
||||||
|
if (error.message !== "xhr poll error") return
|
||||||
|
const toastId = "connect_error"
|
||||||
|
const isActive = toast.isActive(toastId)
|
||||||
|
console.log(toastId, isActive)
|
||||||
|
if (isActive)
|
||||||
|
toast.update(toastId, {
|
||||||
|
autoClose: 5000,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
toast.warn("Es gibt Probleme mit der Echtzeitverbindung.", { toastId })
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("gameSetting", (payload, hash, userId) => {
|
||||||
|
if (userId === session?.user.id) return
|
||||||
|
const newHash = setSetting(payload)
|
||||||
|
if (!newHash || newHash === hash) return
|
||||||
|
console.log("hash", hash, newHash)
|
||||||
|
socket.emit("update", (body) => {
|
||||||
|
console.log("update")
|
||||||
|
full(body)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("playerEvent", (event) => {
|
||||||
|
const { type, i, userId } = event
|
||||||
|
if (userId === session?.user.id) return
|
||||||
|
let message: string
|
||||||
|
console.log(type)
|
||||||
|
switch (type) {
|
||||||
|
case "disconnect":
|
||||||
|
setIsConnected({
|
||||||
|
i,
|
||||||
|
isConnected: false,
|
||||||
|
})
|
||||||
|
message = "Player is disconnected."
|
||||||
|
break
|
||||||
|
|
||||||
|
case "leave":
|
||||||
|
message = "Player has left the lobby."
|
||||||
|
break
|
||||||
|
|
||||||
|
case "connect":
|
||||||
|
setIsConnected({
|
||||||
|
i,
|
||||||
|
isConnected: true,
|
||||||
|
})
|
||||||
|
socket.emit("isReady", userStates[i].isReady)
|
||||||
|
message = "Player has joined the lobby."
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
message = "Not defined yet."
|
||||||
|
break
|
||||||
|
}
|
||||||
|
toast.info(message, { toastId: message })
|
||||||
|
if (type === "disconnect") return
|
||||||
|
const { payload, hash } = event
|
||||||
|
const newHash = setPlayer(payload)
|
||||||
|
console.log(newHash, hash, !newHash, newHash === hash)
|
||||||
|
if (!newHash || newHash === hash) return
|
||||||
|
console.log("hash", hash, newHash)
|
||||||
|
socket.emit("update", (body) => {
|
||||||
|
console.log("update")
|
||||||
|
full(body)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("isReady", (payload, userId) => {
|
||||||
|
if (userId === session?.user.id) return
|
||||||
|
setIsReady(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("disconnect")
|
||||||
|
setIsConnectedState(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.removeAllListeners()
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
full,
|
||||||
|
i,
|
||||||
|
router,
|
||||||
|
session?.user.id,
|
||||||
|
setIsConnected,
|
||||||
|
setIsReady,
|
||||||
|
setPlayer,
|
||||||
|
setSetting,
|
||||||
|
stateHash,
|
||||||
|
])
|
||||||
|
useEffect(
|
||||||
|
() =>
|
||||||
|
console.log(
|
||||||
|
i,
|
||||||
|
isIndex,
|
||||||
|
userStates[i ?? 0].isConnected,
|
||||||
|
isConnectedState,
|
||||||
|
isIndex ? userStates[i].isConnected : isConnectedState,
|
||||||
|
userStates,
|
||||||
|
session?.user.id
|
||||||
|
),
|
||||||
|
[i, isIndex, isConnectedState, session?.user.id, userStates]
|
||||||
|
)
|
||||||
|
useEffect(() => console.log("warst", isConnectedState), [isConnectedState])
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!isConnected) return
|
||||||
|
// let count = 0
|
||||||
|
// const interval = setInterval(() => {
|
||||||
|
// const start = Date.now()
|
||||||
|
// socket.volatile.emit("ping", ++count, (count) => {
|
||||||
|
// const duration = Date.now() - start
|
||||||
|
// console.log("ping", count, duration)
|
||||||
|
// })
|
||||||
|
// }, 5000)
|
||||||
|
// return () => clearInterval(interval)
|
||||||
|
// }, [isConnected])
|
||||||
|
|
||||||
|
return { isConnected: isIndex ? userStates[i].isConnected : isConnectedState }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSocket
|
|
@ -1,10 +1,18 @@
|
||||||
|
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
|
||||||
|
import { GamePropsSchema, PlayerSchema } from "@lib/zodSchemas"
|
||||||
import type { Server as HTTPServer } from "http"
|
import type { Server as HTTPServer } from "http"
|
||||||
import type { NextApiResponse } from "next"
|
|
||||||
import type { Socket as NetSocket } from "net"
|
import type { Socket as NetSocket } from "net"
|
||||||
import type { Server as IOServer } from "socket.io"
|
import type { NextApiResponse } from "next"
|
||||||
|
import { Session } from "next-auth"
|
||||||
|
import type {
|
||||||
|
Server as IOServer,
|
||||||
|
Server,
|
||||||
|
Socket as SocketforServer,
|
||||||
|
} from "socket.io"
|
||||||
|
import type { Socket as SocketforClient } from "socket.io-client"
|
||||||
|
|
||||||
interface SocketServer extends HTTPServer {
|
interface SocketServer extends HTTPServer {
|
||||||
io?: IOServer | undefined
|
io?: IOServer
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SocketWithIO extends NetSocket {
|
interface SocketWithIO extends NetSocket {
|
||||||
|
@ -14,3 +22,82 @@ interface SocketWithIO extends NetSocket {
|
||||||
export interface NextApiResponseWithSocket extends NextApiResponse {
|
export interface NextApiResponseWithSocket extends NextApiResponse {
|
||||||
socket: SocketWithIO
|
socket: SocketWithIO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerToClientEvents {
|
||||||
|
// noArg: () => void
|
||||||
|
// basicEmit: (a: number, b: string, c: Buffer) => void
|
||||||
|
// withAck: (d: string, ) => void
|
||||||
|
gameSetting: (payload: GameSettings, hash: string, userId: string) => void
|
||||||
|
playerEvent: (
|
||||||
|
event:
|
||||||
|
| {
|
||||||
|
type: "connect" | "leave"
|
||||||
|
i: number
|
||||||
|
payload: { users: PlayerSchema[] }
|
||||||
|
hash: string
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "disconnect"
|
||||||
|
i: number
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
) => void
|
||||||
|
isReady: (
|
||||||
|
payload: {
|
||||||
|
i: number
|
||||||
|
isReady: boolean
|
||||||
|
},
|
||||||
|
userId: string
|
||||||
|
) => void
|
||||||
|
isConnected: (
|
||||||
|
payload: {
|
||||||
|
i: number
|
||||||
|
isConnected: boolean
|
||||||
|
},
|
||||||
|
userId: string
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientToServerEvents {
|
||||||
|
update: (callback: (game: GamePropsSchema) => void) => void
|
||||||
|
isReady: (isReady: boolean) => void
|
||||||
|
isConnected: (isReady: boolean) => void
|
||||||
|
ping: (count: number, callback: (count: number) => void) => void
|
||||||
|
join: (withAck: (ack: boolean) => void) => void
|
||||||
|
gameSetting: (payload: GameSettings, callback: (hash: string) => void) => void
|
||||||
|
leave: (withAck: (ack: boolean) => void) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InterServerEvents {
|
||||||
|
// ping: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocketData {
|
||||||
|
props: {
|
||||||
|
userId: string
|
||||||
|
gameId: string
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
user: Session["user"]
|
||||||
|
gameId: string | null
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type sServer = Server<
|
||||||
|
ClientToServerEvents,
|
||||||
|
ServerToClientEvents,
|
||||||
|
InterServerEvents,
|
||||||
|
SocketData
|
||||||
|
>
|
||||||
|
export type sSocket = SocketforServer<
|
||||||
|
ClientToServerEvents,
|
||||||
|
ServerToClientEvents,
|
||||||
|
InterServerEvents,
|
||||||
|
SocketData
|
||||||
|
>
|
||||||
|
|
||||||
|
export type cSocket = SocketforClient<
|
||||||
|
ServerToClientEvents,
|
||||||
|
ClientToServerEvents
|
||||||
|
>
|
||||||
|
|
|
@ -3,7 +3,6 @@ export interface Position {
|
||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
export interface Target extends Position {
|
export interface Target extends Position {
|
||||||
preview: boolean
|
|
||||||
show: boolean
|
show: boolean
|
||||||
}
|
}
|
||||||
export interface MouseCursor extends Position {
|
export interface MouseCursor extends Position {
|
||||||
|
@ -31,12 +30,12 @@ export interface Hit extends Position {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface fireMissile {
|
interface fireMissile {
|
||||||
type: "fireMissile"
|
type: "fireMissile" | "htorpedo" | "vtorpedo"
|
||||||
payload: {
|
payload: {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
hit: boolean
|
hit: boolean
|
||||||
}
|
}[]
|
||||||
}
|
}
|
||||||
interface removeMissile {
|
interface removeMissile {
|
||||||
type: "removeMissile"
|
type: "removeMissile"
|
||||||
|
@ -48,3 +47,14 @@ interface removeMissile {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HitDispatch = fireMissile | removeMissile
|
export type HitDispatch = fireMissile | removeMissile
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Draw {
|
||||||
|
ctx: CanvasRenderingContext2D
|
||||||
|
currentPoint: Point
|
||||||
|
prevPoint: Point | null
|
||||||
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { Player } from "@prisma/client"
|
|
||||||
import bcrypt from "bcrypt"
|
|
||||||
|
|
||||||
export default async function checkPasswordIsValid<T>(
|
|
||||||
payload: T & { player: Player; password: string }
|
|
||||||
) {
|
|
||||||
const { player, password } = payload
|
|
||||||
|
|
||||||
// Validate for correct password
|
|
||||||
return bcrypt.compare(password, player.passwordHash).then(async (result) => {
|
|
||||||
if (!result) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: "Passwords do not match!",
|
|
||||||
statusCode: 401,
|
|
||||||
solved: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
passwordIsValid: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { Token } from "@prisma/client"
|
|
||||||
import jwt from "jsonwebtoken"
|
|
||||||
import jwtVerifyCatch from "../jwtVerifyCatch"
|
|
||||||
|
|
||||||
async function checkTokenIsValid<T>(
|
|
||||||
payload: T & { token: string; tokenType: Token["type"] }
|
|
||||||
) {
|
|
||||||
const { token, tokenType } = payload
|
|
||||||
|
|
||||||
// Verify the token and get the payload
|
|
||||||
let tokenData: string | jwt.JwtPayload
|
|
||||||
try {
|
|
||||||
tokenData = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET as string)
|
|
||||||
} catch (err: any) {
|
|
||||||
// Deal with the problem in more detail
|
|
||||||
return Promise.reject(jwtVerifyCatch(tokenType, err))
|
|
||||||
}
|
|
||||||
// Making sure the token data is not a string (because it should be an object)
|
|
||||||
if (typeof tokenData === "string") {
|
|
||||||
return Promise.reject({
|
|
||||||
message: tokenType + "-Token data was a string. Token: " + token,
|
|
||||||
statusCode: 401,
|
|
||||||
solved: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...payload, tokenBody: token, tokenIsValid: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default checkTokenIsValid
|
|
|
@ -1,28 +0,0 @@
|
||||||
import prisma from "../../prisma"
|
|
||||||
|
|
||||||
async function createAnonymousDB<T>(payload: T) {
|
|
||||||
const player = await prisma.player.create({
|
|
||||||
data: {
|
|
||||||
anonymous: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// .catch((err: any) => {
|
|
||||||
// if (err.code === 11000) {
|
|
||||||
// return Promise.reject({
|
|
||||||
// message: `Duplicate key error while creating Player in DB!`,
|
|
||||||
// statusCode: 409,
|
|
||||||
// solved: true,
|
|
||||||
// type: 'warn'
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// console.log(err)
|
|
||||||
// return Promise.reject({
|
|
||||||
// message: `Unknown error while creating Player in DB.`,
|
|
||||||
// solved: false
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
return { ...payload, player }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createAnonymousDB
|
|
|
@ -1,38 +0,0 @@
|
||||||
import bcrypt from "bcrypt"
|
|
||||||
import prisma from "../../prisma"
|
|
||||||
|
|
||||||
async function createPlayerDB<T>(
|
|
||||||
payload: T & { username: string; password: string }
|
|
||||||
) {
|
|
||||||
const { username, password } = payload
|
|
||||||
|
|
||||||
return await prisma.player
|
|
||||||
.create({
|
|
||||||
data: {
|
|
||||||
username,
|
|
||||||
passwordHash: await bcrypt.hash(password, 10),
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((player) => {
|
|
||||||
return { ...payload, player }
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
if (err.code === 11000) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: `Duplicate key error while creating Player in DB!`,
|
|
||||||
statusCode: 409,
|
|
||||||
solved: true,
|
|
||||||
type: "warn",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log(err)
|
|
||||||
return Promise.reject({
|
|
||||||
message: `Unknown error while creating Player in DB.`,
|
|
||||||
solved: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createPlayerDB
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { Player, Token } from "@prisma/client"
|
|
||||||
import jwt from "jsonwebtoken"
|
|
||||||
import { v4 as uuidv4 } from "uuid"
|
|
||||||
import prisma from "../../prisma"
|
|
||||||
|
|
||||||
const tokenLifetime = {
|
|
||||||
REFRESH: 172800,
|
|
||||||
ACCESS: 15,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function createTokenDB<T>(
|
|
||||||
payload: T & { player: Player; newTokenType: Token["type"] }
|
|
||||||
) {
|
|
||||||
const { player, newTokenType } = payload
|
|
||||||
|
|
||||||
// Sign a new access token
|
|
||||||
const newToken = jwt.sign(
|
|
||||||
{ uuid: uuidv4(), user: player.id },
|
|
||||||
process.env.ACCESS_TOKEN_SECRET as string,
|
|
||||||
{ expiresIn: tokenLifetime[newTokenType] }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Save token to DB
|
|
||||||
const newTokenDB = await prisma.token.create({
|
|
||||||
data: {
|
|
||||||
token: newToken,
|
|
||||||
type: newTokenType,
|
|
||||||
expires: new Date(Date.now() + tokenLifetime[newTokenType] + "000"),
|
|
||||||
owner: {
|
|
||||||
connect: {
|
|
||||||
id: player.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
newToken,
|
|
||||||
newTokenDB,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { Token } from "@prisma/client"
|
|
||||||
import prisma from "../../prisma"
|
|
||||||
|
|
||||||
export default async function getPlayerByIdDB<T>(
|
|
||||||
payload: T & { tokenDB: Token }
|
|
||||||
) {
|
|
||||||
const { tokenDB } = payload
|
|
||||||
// Find Host in DB if it still exists (just to make sure)
|
|
||||||
const player = await prisma.player.findUnique({
|
|
||||||
where: {
|
|
||||||
id: tokenDB.ownerId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!player) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: "Player not found in DB!",
|
|
||||||
statusCode: 401,
|
|
||||||
solved: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
player,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import prisma from "../../prisma"
|
|
||||||
|
|
||||||
export default async function getPlayerByNameDB<T>(
|
|
||||||
payload: T & { username: string }
|
|
||||||
) {
|
|
||||||
const { username } = payload
|
|
||||||
// Find Player in DB if it still exists (just to make sure)
|
|
||||||
const player = await Promise.any([
|
|
||||||
prisma.player.findUnique({
|
|
||||||
where: {
|
|
||||||
username: username,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.player.findUnique({
|
|
||||||
where: {
|
|
||||||
email: username,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
if (!player) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: "Player not found in DB!",
|
|
||||||
statusCode: 401,
|
|
||||||
solved: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
player,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import prisma from "../../prisma"
|
|
||||||
|
|
||||||
async function getTokenDB<T>(
|
|
||||||
payload: T & {
|
|
||||||
tokenBody: string
|
|
||||||
tokenIsValid: boolean
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<any>
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const { tokenBody } = payload
|
|
||||||
|
|
||||||
// Find refresh token in DB
|
|
||||||
const tokenDB = await prisma.token.findUnique({
|
|
||||||
where: {
|
|
||||||
token: tokenBody,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!tokenDB) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: "Access-Token not found in DB!",
|
|
||||||
statusCode: 401,
|
|
||||||
solved: true,
|
|
||||||
type: "warn",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenDB.used) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: "DBToken was already used!",
|
|
||||||
statusCode: 401,
|
|
||||||
solved: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.token.update({
|
|
||||||
where: {
|
|
||||||
token: tokenBody,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
used: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// await logging('Old token has been invalidated.', ['debug'], req)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
tokenDB,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getTokenDB
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { NextApiRequest } from "next"
|
|
||||||
|
|
||||||
async function getTokenFromBody<T>(payload: T & { req: NextApiRequest }) {
|
|
||||||
const { req } = payload
|
|
||||||
const token: string = req.body.token
|
|
||||||
|
|
||||||
// Checking for cookie presens, because it is necessary
|
|
||||||
if (!token) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: "Unauthorized. No Access-Token.",
|
|
||||||
statusCode: 401,
|
|
||||||
solved: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...payload, token }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getTokenFromBody
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { Token } from "@prisma/client"
|
|
||||||
import { NextApiRequest } from "next"
|
|
||||||
|
|
||||||
async function getTokenFromCookie<T>(payload: T & { req: NextApiRequest }) {
|
|
||||||
const { req } = payload
|
|
||||||
const token = req.cookies.token
|
|
||||||
|
|
||||||
// Checking for cookie presens, because it is necessary
|
|
||||||
if (!token) {
|
|
||||||
return Promise.reject({
|
|
||||||
message: "Unauthorized. No cookie.",
|
|
||||||
statusCode: 401,
|
|
||||||
solved: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...payload, token, tokenType: "REFRESH" as Token["type"] }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getTokenFromCookie
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { Token } from "@prisma/client"
|
|
||||||
import { Logging } from "../logging"
|
|
||||||
|
|
||||||
export default async function loginCheck<T>(
|
|
||||||
payload: T & { loginCheck: boolean; tokenDB: Token; tokenType: "REFRESH" }
|
|
||||||
) {
|
|
||||||
const { loginCheck, tokenDB } = payload
|
|
||||||
// True login check response
|
|
||||||
if (loginCheck) {
|
|
||||||
return Promise.resolve({
|
|
||||||
message: "loginCheck " + loginCheck + " of " + tokenDB.id,
|
|
||||||
body: { loggedIn: true },
|
|
||||||
type: ["debug", "info.cyan"] as Logging[],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return payload
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import logging from "../logging"
|
|
||||||
|
|
||||||
export default function sendError<T>(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<T>,
|
|
||||||
err: any
|
|
||||||
) {
|
|
||||||
// If something went wrong, let the client know with status 500
|
|
||||||
res.status(err.statusCode ?? 500).end()
|
|
||||||
logging(err.message, [err.type ?? (err.solved ? "debug" : "error")], req)
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import logging, { Logging } from "../logging"
|
|
||||||
|
|
||||||
export interface Result<T> {
|
|
||||||
message: string
|
|
||||||
statusCode?: number
|
|
||||||
body?: T
|
|
||||||
type?: Logging[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function sendResponse<T>(payload: {
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<T>
|
|
||||||
result: Result<T>
|
|
||||||
}) {
|
|
||||||
const { req, res, result } = payload
|
|
||||||
res.status(result.statusCode ?? 200)
|
|
||||||
result.body ? res.json(result.body) : res.end()
|
|
||||||
logging(result.message, result.type ?? ["debug"], req)
|
|
||||||
}
|
|
24
leaky-ships/lib/backend/errors.ts
Normal file
24
leaky-ships/lib/backend/errors.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { Logging } from "./logging"
|
||||||
|
|
||||||
|
export interface rejectionError {
|
||||||
|
message: string
|
||||||
|
statusCode: number
|
||||||
|
solved: boolean
|
||||||
|
type?: Logging[]
|
||||||
|
}
|
||||||
|
interface rejectionErrors {
|
||||||
|
[key: string]: rejectionError
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rejectionErrors: rejectionErrors = {
|
||||||
|
gameNotFound: {
|
||||||
|
message: "Game not found!",
|
||||||
|
statusCode: 404,
|
||||||
|
solved: true,
|
||||||
|
},
|
||||||
|
unauthorized: {
|
||||||
|
message: "Unauthorized",
|
||||||
|
statusCode: 401,
|
||||||
|
solved: true,
|
||||||
|
},
|
||||||
|
}
|
24
leaky-ships/lib/backend/getPinFromBody.ts
Normal file
24
leaky-ships/lib/backend/getPinFromBody.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import sendError from "./sendError"
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
const pinBodySchema = z.object({
|
||||||
|
pin: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getPinFromBody<T>(req: NextApiRequest, res: NextApiResponse<T>) {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(req.body)
|
||||||
|
const { pin } = pinBodySchema.parse(body)
|
||||||
|
return pin
|
||||||
|
} catch (err: any) {
|
||||||
|
sendError(req, res, {
|
||||||
|
message: "No pin in request body!",
|
||||||
|
statusCode: 401,
|
||||||
|
solved: true,
|
||||||
|
type: ["warn"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getPinFromBody
|
|
@ -1,36 +0,0 @@
|
||||||
import { Token } from "@prisma/client"
|
|
||||||
|
|
||||||
export default async function jwtVerifyCatch(
|
|
||||||
tokenType: Token["type"],
|
|
||||||
err: Error
|
|
||||||
) {
|
|
||||||
switch (err.message) {
|
|
||||||
case "jwt expired":
|
|
||||||
return {
|
|
||||||
message: `JWT (${tokenType}) expired!`,
|
|
||||||
statusCode: 403,
|
|
||||||
solved: true,
|
|
||||||
type: "warn",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "invalid signature":
|
|
||||||
return {
|
|
||||||
message: `Invalid JWT (${tokenType}) signature! Token: `,
|
|
||||||
statusCode: 401,
|
|
||||||
solved: true,
|
|
||||||
type: "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "jwt must be provided":
|
|
||||||
return {
|
|
||||||
message: `No JWT (${tokenType}) given.`,
|
|
||||||
statusCode: 401,
|
|
||||||
solved: true,
|
|
||||||
type: "warn",
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log(err)
|
|
||||||
return { message: `Unknown error on 'JWT.verify()'.`, solved: false }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,22 @@
|
||||||
import fs from "fs"
|
|
||||||
import colors, { Color } from "colors"
|
import colors, { Color } from "colors"
|
||||||
import { NextApiRequest } from "next"
|
import fs from "fs"
|
||||||
import { IncomingMessage } from "http"
|
import { IncomingMessage } from "http"
|
||||||
|
import { NextApiRequest } from "next"
|
||||||
|
|
||||||
colors.enable()
|
colors.enable()
|
||||||
|
|
||||||
const loggingTemplates: { [key: string]: LoggingType } = {
|
const loggingTemplates: {
|
||||||
|
system: LoggingType
|
||||||
|
infoGreen: LoggingType
|
||||||
|
infoCyan: LoggingType
|
||||||
|
debug: LoggingType
|
||||||
|
post: LoggingType
|
||||||
|
warn: LoggingType
|
||||||
|
error: LoggingType
|
||||||
|
} = {
|
||||||
system: ["SYSTEM", "green"],
|
system: ["SYSTEM", "green"],
|
||||||
"info.green": ["INFO", "green"],
|
infoGreen: ["INFO", "green"],
|
||||||
"info.cyan": ["INFO", "cyan"],
|
infoCyan: ["INFO", "cyan"],
|
||||||
debug: ["Debug", "grey"],
|
debug: ["Debug", "grey"],
|
||||||
post: ["Post", "white"],
|
post: ["Post", "white"],
|
||||||
warn: ["WARN", "yellow"],
|
warn: ["WARN", "yellow"],
|
||||||
|
@ -22,7 +31,7 @@ let started: boolean = false
|
||||||
async function logStartup() {
|
async function logStartup() {
|
||||||
await fs.promises.stat("log").catch(async () => {
|
await fs.promises.stat("log").catch(async () => {
|
||||||
await fs.promises.mkdir("log")
|
await fs.promises.mkdir("log")
|
||||||
await logging(`Created 'log' Folder.`, ["info.cyan", "system"])
|
await logging(`Created 'log' Folder.`, ["infoCyan", "system"])
|
||||||
})
|
})
|
||||||
started = true
|
started = true
|
||||||
}
|
}
|
||||||
|
@ -49,8 +58,11 @@ async function logging(
|
||||||
if (req) {
|
if (req) {
|
||||||
const forwardedFor: any = req.headers["x-forwarded-for"]
|
const forwardedFor: any = req.headers["x-forwarded-for"]
|
||||||
const ip = (forwardedFor || "127.0.0.1, 192.168.178.1").split(",")
|
const ip = (forwardedFor || "127.0.0.1, 192.168.178.1").split(",")
|
||||||
messages.console = ip[0].yellow + " - " + messages.console
|
const route = req.url
|
||||||
messages.file = ip[0] + " - " + messages.file
|
messages.console = [ip[0].yellow, route?.green, messages.console].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)
|
||||||
|
|
20
leaky-ships/lib/backend/sendError.ts
Normal file
20
leaky-ships/lib/backend/sendError.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { rejectionError } from "./errors"
|
||||||
|
import logging from "./logging"
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
export default function sendError<T>(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<T>,
|
||||||
|
err: rejectionError | Error
|
||||||
|
) {
|
||||||
|
// If something went wrong, let the client know with status 500
|
||||||
|
res.status("statusCode" in err ? err.statusCode : 500).end()
|
||||||
|
logging(
|
||||||
|
err.message,
|
||||||
|
"type" in err && err.type
|
||||||
|
? err.type
|
||||||
|
: ["solved" in err && err.solved ? "debug" : "error"],
|
||||||
|
req
|
||||||
|
)
|
||||||
|
if ("name" in err) console.log(err)
|
||||||
|
}
|
25
leaky-ships/lib/backend/sendResponse.ts
Normal file
25
leaky-ships/lib/backend/sendResponse.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import logging, { Logging } from "./logging"
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
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,29 +0,0 @@
|
||||||
import { GetServerSidePropsContext, PreviewData } from "next"
|
|
||||||
import { ParsedUrlQuery } from "querystring"
|
|
||||||
import getTokenFromCookie from "../backend/components/getTokenFromCookie"
|
|
||||||
import checkTokenIsValid from "../backend/components/checkTokenIsValid"
|
|
||||||
import getTokenDB from "../backend/components/getTokenDB"
|
|
||||||
import getPlayerByIdDB from "../backend/components/getPlayerByIdDB"
|
|
||||||
import logging from "../backend/logging"
|
|
||||||
|
|
||||||
export default async function checkIsLoggedIn(
|
|
||||||
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>
|
|
||||||
) {
|
|
||||||
const req: any = context.req
|
|
||||||
const res: any = context.res
|
|
||||||
|
|
||||||
const isLoggedIn = await getTokenFromCookie({ req, res })
|
|
||||||
.then(checkTokenIsValid)
|
|
||||||
.then(getTokenDB)
|
|
||||||
.then(getPlayerByIdDB)
|
|
||||||
.then(({ player }) => !!player)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
logging(
|
|
||||||
"loginCheck " + (isLoggedIn ? true : "-> loggedIn: " + false),
|
|
||||||
["debug", "info.cyan"],
|
|
||||||
req
|
|
||||||
)
|
|
||||||
|
|
||||||
return isLoggedIn
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export default function getAccessToken(): Promise<string> {
|
|
||||||
return fetch("/api/auth", {
|
|
||||||
method: "GET",
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((res) => res.newAccessToken)
|
|
||||||
}
|
|
10
leaky-ships/lib/getPayloadwithChecksum.ts
Normal file
10
leaky-ships/lib/getPayloadwithChecksum.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { GamePropsSchema } from "./zodSchemas"
|
||||||
|
import crypto from "crypto"
|
||||||
|
|
||||||
|
export function getPayloadwithChecksum(
|
||||||
|
payload: GamePropsSchema["payload"]
|
||||||
|
): GamePropsSchema {
|
||||||
|
const objString = JSON.stringify(payload)
|
||||||
|
const hash = crypto.createHash("md5").update(objString).digest("hex")
|
||||||
|
return { payload, hash }
|
||||||
|
}
|
|
@ -1,222 +0,0 @@
|
||||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react"
|
|
||||||
import {
|
|
||||||
hitReducer,
|
|
||||||
initlialLastLeftTile,
|
|
||||||
initlialTarget,
|
|
||||||
initlialTargetPreview,
|
|
||||||
initlialMouseCursor,
|
|
||||||
} from "../utils/helpers"
|
|
||||||
import {
|
|
||||||
Hit,
|
|
||||||
Mode,
|
|
||||||
MouseCursor,
|
|
||||||
Target,
|
|
||||||
Position,
|
|
||||||
} from "../../interfaces/frontend"
|
|
||||||
import { PointerProps } from "../../components/GamefieldPointer"
|
|
||||||
|
|
||||||
const modes: Mode[] = [
|
|
||||||
{
|
|
||||||
pointerGrid: Array.from(Array(3), () => Array.from(Array(3))),
|
|
||||||
type: "radar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pointerGrid: Array.from(Array(3), () => Array.from(Array(1))),
|
|
||||||
type: "htorpedo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pointerGrid: Array.from(Array(1), () => Array.from(Array(3))),
|
|
||||||
type: "vtorpedo",
|
|
||||||
},
|
|
||||||
{ pointerGrid: [[{ x: 0, y: 0 }]], type: "missile" },
|
|
||||||
]
|
|
||||||
|
|
||||||
function useGameEvent(count: number) {
|
|
||||||
const [lastLeftTile, setLastLeftTile] =
|
|
||||||
useState<Position>(initlialLastLeftTile)
|
|
||||||
const [target, setTarget] = useState<Target>(initlialTarget)
|
|
||||||
const [eventReady, setEventReady] = useState(false)
|
|
||||||
const [appearOK, setAppearOK] = useState(false)
|
|
||||||
const [targetPreview, setTargetPreview] = useState<Target>(
|
|
||||||
initlialTargetPreview
|
|
||||||
)
|
|
||||||
const [mouseCursor, setMouseCursor] =
|
|
||||||
useState<MouseCursor>(initlialMouseCursor)
|
|
||||||
const [hits, DispatchHits] = useReducer(hitReducer, [] as Hit[])
|
|
||||||
const [mode, setMode] = useState(0)
|
|
||||||
|
|
||||||
const targetList = useCallback(
|
|
||||||
(target: Position) => {
|
|
||||||
const { pointerGrid, type } = modes[mode]
|
|
||||||
const xLength = pointerGrid.length
|
|
||||||
const yLength = pointerGrid[0].length
|
|
||||||
const { x: targetX, y: targetY } = target
|
|
||||||
return pointerGrid
|
|
||||||
.map((arr, i) => {
|
|
||||||
return arr.map((_, i2) => {
|
|
||||||
const relativeX = -Math.floor(xLength / 2) + i
|
|
||||||
const relativeY = -Math.floor(yLength / 2) + i2
|
|
||||||
const x = targetX + (relativeX ?? 0)
|
|
||||||
const y = targetY + (relativeY ?? 0)
|
|
||||||
return {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
type,
|
|
||||||
edges: [
|
|
||||||
i === 0 ? "left" : "",
|
|
||||||
i === xLength - 1 ? "right" : "",
|
|
||||||
i2 === 0 ? "top" : "",
|
|
||||||
i2 === yLength - 1 ? "bottom" : "",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.reduce((prev, curr) => [...prev, ...curr], [])
|
|
||||||
},
|
|
||||||
[mode]
|
|
||||||
)
|
|
||||||
|
|
||||||
const isHit = useCallback(
|
|
||||||
(x: number, y: number) => {
|
|
||||||
return hits.filter((h) => h.x === x && h.y === y)
|
|
||||||
},
|
|
||||||
[hits]
|
|
||||||
)
|
|
||||||
|
|
||||||
const settingTarget = useCallback(
|
|
||||||
(isGameTile: boolean, x: number, y: number) => {
|
|
||||||
if (!isGameTile || isHit(x, y).length) return
|
|
||||||
setMouseCursor((e) => ({ ...e, shouldShow: false }))
|
|
||||||
setTarget((t) => {
|
|
||||||
if (t.x === x && t.y === y && t.show) {
|
|
||||||
DispatchHits({
|
|
||||||
type: "fireMissile",
|
|
||||||
payload: { hit: (x + y) % 2 !== 0, x, y },
|
|
||||||
})
|
|
||||||
return { preview: false, show: false, x, y }
|
|
||||||
} else {
|
|
||||||
const target = { preview: false, show: true, x, y }
|
|
||||||
const hasAnyBorder = targetList(target).filter(({ x, y }) =>
|
|
||||||
isBorder(x, y, count)
|
|
||||||
).length
|
|
||||||
if (hasAnyBorder) return t
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[count, isHit, targetList]
|
|
||||||
)
|
|
||||||
|
|
||||||
const isSet = useCallback(
|
|
||||||
(x: number, y: number) => {
|
|
||||||
return (
|
|
||||||
!!targetList(target).filter((field) => x === field.x && y === field.y)
|
|
||||||
.length && target.show
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[target, targetList]
|
|
||||||
)
|
|
||||||
|
|
||||||
const composeTargetTiles = useCallback(
|
|
||||||
(target: Target): PointerProps[] => {
|
|
||||||
const { preview, show } = target
|
|
||||||
const result = targetList(target).map(({ x, y, type, edges }) => {
|
|
||||||
return {
|
|
||||||
preview,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
show,
|
|
||||||
type,
|
|
||||||
edges,
|
|
||||||
imply: !!isHit(x, y).length || (!!isSet(x, y) && preview),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
[isHit, isSet, targetList]
|
|
||||||
)
|
|
||||||
|
|
||||||
// handle visibility and position change of targetPreview
|
|
||||||
useEffect(() => {
|
|
||||||
const { show, x, y } = targetPreview
|
|
||||||
// if mouse has moved too quickly and last event was entering and leaving the same field, it must have gone outside the grid
|
|
||||||
const hasLeft = x === lastLeftTile.x && y === lastLeftTile.y
|
|
||||||
const isSet = x === target.x && y === target.y && target.show
|
|
||||||
|
|
||||||
if (show && !appearOK) setTargetPreview((e) => ({ ...e, show: false }))
|
|
||||||
if (
|
|
||||||
!show &&
|
|
||||||
mouseCursor.shouldShow &&
|
|
||||||
eventReady &&
|
|
||||||
appearOK &&
|
|
||||||
!isHit(x, y).length &&
|
|
||||||
!isSet &&
|
|
||||||
!hasLeft
|
|
||||||
)
|
|
||||||
setTargetPreview((e) => ({ ...e, show: true }))
|
|
||||||
}, [
|
|
||||||
targetPreview,
|
|
||||||
mouseCursor.shouldShow,
|
|
||||||
isHit,
|
|
||||||
eventReady,
|
|
||||||
appearOK,
|
|
||||||
lastLeftTile,
|
|
||||||
target,
|
|
||||||
])
|
|
||||||
|
|
||||||
// enable targetPreview event again after 200 ms.
|
|
||||||
useEffect(() => {
|
|
||||||
setEventReady(false)
|
|
||||||
const previewTarget = { x: mouseCursor.x, y: mouseCursor.y }
|
|
||||||
const hasAnyBorder = targetList(previewTarget).filter(({ x, y }) =>
|
|
||||||
isBorder(x, y, count)
|
|
||||||
).length
|
|
||||||
if (targetPreview.show || !appearOK || hasAnyBorder) return
|
|
||||||
const autoTimeout = setTimeout(() => {
|
|
||||||
setTargetPreview((e) => ({ ...e, ...previewTarget }))
|
|
||||||
setEventReady(true)
|
|
||||||
setAppearOK(true)
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
// or abort if state has changed early
|
|
||||||
return () => {
|
|
||||||
clearTimeout(autoTimeout)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
appearOK,
|
|
||||||
count,
|
|
||||||
mouseCursor.x,
|
|
||||||
mouseCursor.y,
|
|
||||||
targetList,
|
|
||||||
targetPreview.show,
|
|
||||||
])
|
|
||||||
|
|
||||||
// approve targetPreview new position after 200 mil. sec.
|
|
||||||
useEffect(() => {
|
|
||||||
// early return to start cooldown only when about to show up
|
|
||||||
const autoTimeout = setTimeout(
|
|
||||||
() => {
|
|
||||||
setAppearOK(!targetPreview.show)
|
|
||||||
},
|
|
||||||
targetPreview.show ? 500 : 300
|
|
||||||
)
|
|
||||||
|
|
||||||
// or abort if movement is repeated early
|
|
||||||
return () => {
|
|
||||||
clearTimeout(autoTimeout)
|
|
||||||
}
|
|
||||||
}, [targetPreview.show])
|
|
||||||
|
|
||||||
return {
|
|
||||||
tilesProps: { count, settingTarget, setMouseCursor, setLastLeftTile },
|
|
||||||
pointersProps: { composeTargetTiles, target, targetPreview },
|
|
||||||
targetsProps: { setMode, setTarget },
|
|
||||||
hits,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBorder(x: number, y: number, count: number) {
|
|
||||||
return x < 2 || x > count + 1 || y < 2 || y > count + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useGameEvent
|
|
7
leaky-ships/lib/socket.ts
Normal file
7
leaky-ships/lib/socket.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { cSocket } from "../interfaces/NextApiSocket"
|
||||||
|
import { io } from "socket.io-client"
|
||||||
|
|
||||||
|
export const socket: cSocket = io({
|
||||||
|
path: "/api/ws",
|
||||||
|
autoConnect: false,
|
||||||
|
})
|
|
@ -1,4 +1,13 @@
|
||||||
import { Hit, HitDispatch } from "../../interfaces/frontend"
|
import type {
|
||||||
|
Hit,
|
||||||
|
HitDispatch,
|
||||||
|
Mode,
|
||||||
|
Position,
|
||||||
|
Target,
|
||||||
|
TargetList,
|
||||||
|
} from "../../interfaces/frontend"
|
||||||
|
import { count } from "@components/Gamefield/Gamefield"
|
||||||
|
import { PointerProps } from "@components/Gamefield/GamefieldPointer"
|
||||||
|
|
||||||
export function borderCN(count: number, x: number, y: number) {
|
export function borderCN(count: number, x: number, y: number) {
|
||||||
if (x === 0) return "left"
|
if (x === 0) return "left"
|
||||||
|
@ -19,8 +28,10 @@ export function fieldIndex(count: number, x: number, y: number) {
|
||||||
}
|
}
|
||||||
export function hitReducer(formObject: Hit[], action: HitDispatch) {
|
export function hitReducer(formObject: Hit[], action: HitDispatch) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "fireMissile": {
|
case "fireMissile":
|
||||||
const result = [...formObject, action.payload]
|
case "htorpedo":
|
||||||
|
case "vtorpedo": {
|
||||||
|
const result = [...formObject, ...action.payload]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,10 +39,92 @@ export function hitReducer(formObject: Hit[], action: HitDispatch) {
|
||||||
return formObject
|
return formObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const initlialLastLeftTile = {
|
|
||||||
x: 0,
|
const modes: Mode[] = [
|
||||||
y: 0,
|
{
|
||||||
|
pointerGrid: Array.from(Array(3), () => Array.from(Array(3))),
|
||||||
|
type: "radar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pointerGrid: Array.from(Array(3), () => Array.from(Array(1))),
|
||||||
|
type: "htorpedo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pointerGrid: Array.from(Array(1), () => Array.from(Array(3))),
|
||||||
|
type: "vtorpedo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pointerGrid: Array.from(Array(1), () => Array.from(Array(1))),
|
||||||
|
type: "missile",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function isBorder(x: number, y: number, count: number) {
|
||||||
|
return x < 2 || x > count + 1 || y < 2 || y > count + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAlreadyHit(x: number, y: number, hits: Hit[]) {
|
||||||
|
return !!hits.filter((h) => h.x === x && h.y === y).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(x: number, y: number, targetList: TargetList[], show: boolean) {
|
||||||
|
return (
|
||||||
|
!!targetList.filter((field) => x === field.x && y === field.y).length &&
|
||||||
|
show
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function targetList(
|
||||||
|
{ x: targetX, y: targetY }: Position,
|
||||||
|
mode: number
|
||||||
|
): TargetList[] {
|
||||||
|
const { pointerGrid, type } = modes[mode]
|
||||||
|
const xLength = pointerGrid.length
|
||||||
|
const yLength = pointerGrid[0].length
|
||||||
|
return pointerGrid
|
||||||
|
.map((arr, i) => {
|
||||||
|
return arr.map((_, i2) => {
|
||||||
|
const relativeX = -Math.floor(xLength / 2) + i
|
||||||
|
const relativeY = -Math.floor(yLength / 2) + i2
|
||||||
|
const x = targetX + (relativeX ?? 0)
|
||||||
|
const y = targetY + (relativeY ?? 0)
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
type,
|
||||||
|
edges: [
|
||||||
|
i === 0 ? "left" : "",
|
||||||
|
i === xLength - 1 ? "right" : "",
|
||||||
|
i2 === 0 ? "top" : "",
|
||||||
|
i2 === yLength - 1 ? "bottom" : "",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.reduce((prev, curr) => [...prev, ...curr], [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function overlapsWithAnyBorder(target: Position, mode: number) {
|
||||||
|
return !!targetList(target, mode).filter(({ x, y }) => isBorder(x, y, count))
|
||||||
|
.length
|
||||||
|
}
|
||||||
|
|
||||||
|
export function composeTargetTiles(
|
||||||
|
target: Target,
|
||||||
|
mode: number,
|
||||||
|
hits: Hit[]
|
||||||
|
): PointerProps[] {
|
||||||
|
const { show } = target
|
||||||
|
return targetList(target, mode).map((targetItem) => {
|
||||||
|
const { x, y } = targetItem
|
||||||
|
return {
|
||||||
|
...targetItem,
|
||||||
|
show,
|
||||||
|
imply: isAlreadyHit(x, y, hits),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const initlialTarget = {
|
export const initlialTarget = {
|
||||||
preview: false,
|
preview: false,
|
||||||
show: false,
|
show: false,
|
||||||
|
|
53
leaky-ships/lib/zodSchemas.ts
Normal file
53
leaky-ships/lib/zodSchemas.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { GameState } from "@prisma/client"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const PlayerSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
index: z.number(),
|
||||||
|
chats: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
event: z.string().nullable(),
|
||||||
|
message: z.string().nullable(),
|
||||||
|
createdAt: z.coerce.date(),
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
moves: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
|
||||||
|
export type PlayerSchema = z.infer<typeof PlayerSchema>
|
||||||
|
|
||||||
|
export const CreateSchema = z.object({
|
||||||
|
game: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
state: z.nativeEnum(GameState),
|
||||||
|
allowSpectators: z.boolean(),
|
||||||
|
allowSpecials: z.boolean(),
|
||||||
|
allowChat: z.boolean(),
|
||||||
|
allowMarkDraw: z.boolean(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
gamePin: z.string().nullable(),
|
||||||
|
users: PlayerSchema.array(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const GamePropsSchema = z.object({
|
||||||
|
payload: CreateSchema,
|
||||||
|
hash: z.string(),
|
||||||
|
})
|
||||||
|
export const optionalGamePropsSchema = z.object({
|
||||||
|
payload: CreateSchema.nullable(),
|
||||||
|
hash: z.string().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type GamePropsSchema = z.infer<typeof GamePropsSchema>
|
||||||
|
export type optionalGamePropsSchema = z.infer<typeof optionalGamePropsSchema>
|
|
@ -9,46 +9,52 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/pro-duotone-svg-icons": "^6.2.1",
|
"@fortawesome/pro-duotone-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/pro-light-svg-icons": "^6.2.1",
|
"@fortawesome/pro-light-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/pro-regular-svg-icons": "^6.2.1",
|
"@fortawesome/pro-regular-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/pro-solid-svg-icons": "^6.2.1",
|
"@fortawesome/pro-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/pro-thin-svg-icons": "^6.2.1",
|
"@fortawesome/pro-thin-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@fortawesome/sharp-solid-svg-icons": "^6.2.1",
|
"@fortawesome/sharp-solid-svg-icons": "^6.4.0",
|
||||||
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@next/font": "13.1.1",
|
"@next/font": "13.1.1",
|
||||||
"@prisma/client": "^4.9.0",
|
"@prisma/client": "^4.15.0",
|
||||||
"bcrypt": "^5.1.0",
|
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"cookies-next": "^2.1.1",
|
|
||||||
"eslint": "8.31.0",
|
"eslint": "8.31.0",
|
||||||
"eslint-config-next": "13.1.1",
|
"eslint-config-next": "13.1.1",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"http-status": "^1.6.2",
|
||||||
|
"immer": "^10.0.2",
|
||||||
"next": "13.1.1",
|
"next": "13.1.1",
|
||||||
"prisma": "^4.9.0",
|
"next-auth": "^4.22.1",
|
||||||
|
"nodemailer": "^6.9.3",
|
||||||
|
"prisma": "^4.15.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"socket.io": "^4.5.4",
|
"react-otp-input": "^3.0.2",
|
||||||
"socket.io-client": "^4.5.4",
|
"react-toastify": "^9.1.3",
|
||||||
|
"socket.io": "^4.6.2",
|
||||||
|
"socket.io-client": "^4.6.2",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"uuid": "^9.0.0"
|
"unique-names-generator": "^4.7.1",
|
||||||
|
"zod": "3.21.1",
|
||||||
|
"zod-prisma-types": "^2.7.1",
|
||||||
|
"zustand": "^4.3.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@total-typescript/ts-reset": "^0.3.7",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.16.16",
|
||||||
"@types/react": "^18.0.27",
|
"@types/react": "^18.2.8",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.2.4",
|
||||||
"@types/uuid": "^9.0.0",
|
|
||||||
"@types/web-bluetooth": "^0.0.16",
|
"@types/web-bluetooth": "^0.0.16",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.62.1",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
import "../styles/App.scss"
|
import "../styles/App.scss"
|
||||||
import "../styles/grid.scss"
|
import "../styles/globals.scss"
|
||||||
import "../styles/grid2.scss"
|
import "../styles/grid2.scss"
|
||||||
import "../styles/homepage.scss"
|
import "../styles/grid.scss"
|
||||||
import "../styles/globals.css"
|
import "@fortawesome/fontawesome-svg-core/styles.css"
|
||||||
|
import { SessionProvider } from "next-auth/react"
|
||||||
import type { AppProps } from "next/app"
|
import type { AppProps } from "next/app"
|
||||||
|
import { ToastContainer } from "react-toastify"
|
||||||
|
import "react-toastify/dist/ReactToastify.css"
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({
|
||||||
return <Component {...pageProps} />
|
Component,
|
||||||
|
pageProps: { session, ...pageProps },
|
||||||
|
}: AppProps) {
|
||||||
|
return (
|
||||||
|
<SessionProvider session={session}>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
<ToastContainer />
|
||||||
|
</SessionProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import getTokenFromCookie from "../../lib/backend/components/getTokenFromCookie"
|
|
||||||
import checkTokenIsValid from "../../lib/backend/components/checkTokenIsValid"
|
|
||||||
import getTokenDB from "../../lib/backend/components/getTokenDB"
|
|
||||||
import getPlayerByIdDB from "../../lib/backend/components/getPlayerByIdDB"
|
|
||||||
import createTokenDB from "../../lib/backend/components/createTokenDB"
|
|
||||||
import sendResponse from "../../lib/backend/components/sendResponse"
|
|
||||||
import sendError from "../../lib/backend/components/sendError"
|
|
||||||
import { Logging } from "../../lib/backend/logging"
|
|
||||||
import { Token } from "@prisma/client"
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function auth(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>
|
|
||||||
) {
|
|
||||||
return getTokenFromCookie({
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
newTokenType: "ACCESS" as Token["type"],
|
|
||||||
})
|
|
||||||
.then(checkTokenIsValid)
|
|
||||||
.then(getTokenDB)
|
|
||||||
.then(getPlayerByIdDB)
|
|
||||||
.then(createTokenDB)
|
|
||||||
.then(authResponse<Data>)
|
|
||||||
.then(sendResponse<Data>)
|
|
||||||
.catch((err) => sendError(req, res, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authResponse<T>(payload: {
|
|
||||||
newToken: string
|
|
||||||
newTokenDB: Token
|
|
||||||
tokenDB: Token
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<T>
|
|
||||||
}) {
|
|
||||||
const { newToken, newTokenDB, tokenDB, req, res } = payload
|
|
||||||
|
|
||||||
// Successfull response
|
|
||||||
return {
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
result: {
|
|
||||||
message:
|
|
||||||
"Access-Token generated: " +
|
|
||||||
newTokenDB.id +
|
|
||||||
" with Refreshtoken-Token: " +
|
|
||||||
tokenDB.id,
|
|
||||||
body: { token: newToken },
|
|
||||||
type: ["debug", "info.cyan"] as Logging[],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
56
leaky-ships/pages/api/auth/[...nextauth].ts
Normal file
56
leaky-ships/pages/api/auth/[...nextauth].ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import prisma from "@lib/prisma"
|
||||||
|
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||||
|
import { NextApiHandler } from "next"
|
||||||
|
import NextAuth, { NextAuthOptions } from "next-auth"
|
||||||
|
import AzureADProvider from "next-auth/providers/azure-ad"
|
||||||
|
import EmailProvider from "next-auth/providers/email"
|
||||||
|
import {
|
||||||
|
uniqueNamesGenerator,
|
||||||
|
Config,
|
||||||
|
animals,
|
||||||
|
NumberDictionary,
|
||||||
|
} from "unique-names-generator"
|
||||||
|
|
||||||
|
const numberDictionary = NumberDictionary.generate({ min: 0, max: 9999 })
|
||||||
|
const customConfig: Config = {
|
||||||
|
dictionaries: [animals, numberDictionary],
|
||||||
|
separator: " ",
|
||||||
|
style: "capital",
|
||||||
|
length: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
EmailProvider({
|
||||||
|
server: process.env.EMAIL_SERVER,
|
||||||
|
from: process.env.EMAIL_FROM,
|
||||||
|
}),
|
||||||
|
AzureADProvider({
|
||||||
|
clientId: process.env.AZURE_AD_CLIENT_ID ?? "",
|
||||||
|
clientSecret: process.env.AZURE_AD_CLIENT_SECRET ?? "",
|
||||||
|
tenantId: process.env.AZURE_AD_TENANT_ID,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
adapter: PrismaAdapter(prisma),
|
||||||
|
secret: process.env.SECRET,
|
||||||
|
callbacks: {
|
||||||
|
signIn: ({ user, account }) => {
|
||||||
|
// Custom signIn callback to add username to email provider
|
||||||
|
if (account && account.provider === "email") {
|
||||||
|
user.name = uniqueNamesGenerator(customConfig) // Replace with your desired username
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
session: ({ session, user }) => {
|
||||||
|
if (session?.user) {
|
||||||
|
session.user.id = user.id
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { options as authOptions }
|
||||||
|
|
||||||
|
const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options)
|
||||||
|
export default authHandler
|
|
@ -1,52 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import getTokenFromBody from "../../lib/backend/components/getTokenFromBody"
|
|
||||||
import checkTokenIsValid from "../../lib/backend/components/checkTokenIsValid"
|
|
||||||
import getTokenDB from "../../lib/backend/components/getTokenDB"
|
|
||||||
import getPlayerByIdDB from "../../lib/backend/components/getPlayerByIdDB"
|
|
||||||
import sendResponse from "../../lib/backend/components/sendResponse"
|
|
||||||
import sendError from "../../lib/backend/components/sendError"
|
|
||||||
import { Logging } from "../../lib/backend/logging"
|
|
||||||
import { Game, Player, Token } from "@prisma/client"
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
games: Game[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function data(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>
|
|
||||||
) {
|
|
||||||
return getTokenFromBody({ req, res, tokenType: "ACCESS" as Token["type"] })
|
|
||||||
.then(checkTokenIsValid)
|
|
||||||
.then(getTokenDB)
|
|
||||||
.then(getPlayerByIdDB)
|
|
||||||
.then(dataResponse<Data>)
|
|
||||||
.then(sendResponse<Data>)
|
|
||||||
.catch((err) => sendError(req, res, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dataResponse<T>(payload: {
|
|
||||||
player: Player
|
|
||||||
tokenDB: Token
|
|
||||||
// games: Game[],
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<T>
|
|
||||||
}) {
|
|
||||||
const { player, tokenDB, req, res } = payload
|
|
||||||
|
|
||||||
const games: any = {}
|
|
||||||
// Successfull response
|
|
||||||
return {
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
result: {
|
|
||||||
message:
|
|
||||||
"Requested data of user: " +
|
|
||||||
player.id +
|
|
||||||
" with Access-Token: " +
|
|
||||||
tokenDB.id,
|
|
||||||
body: { games },
|
|
||||||
type: ["debug", "info.cyan"] as Logging[],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
46
leaky-ships/pages/api/game/[id].ts
Normal file
46
leaky-ships/pages/api/game/[id].ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { authOptions } from "../auth/[...nextauth]"
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
}
|
66
leaky-ships/pages/api/game/create.ts
Normal file
66
leaky-ships/pages/api/game/create.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { authOptions } from "../auth/[...nextauth]"
|
||||||
|
import { composeBody, gameSelects, getAnyRunningGame } from "./running"
|
||||||
|
import sendResponse from "@backend/sendResponse"
|
||||||
|
import { rejectionErrors } from "@lib/backend/errors"
|
||||||
|
import prisma from "@lib/prisma"
|
||||||
|
import { GamePropsSchema } from "@lib/zodSchemas"
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
import { getServerSession } from "next-auth"
|
||||||
|
|
||||||
|
export default async function create(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<GamePropsSchema>
|
||||||
|
) {
|
||||||
|
const session = await getServerSession(req, res, authOptions)
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return sendResponse(req, res, rejectionErrors.unauthorized)
|
||||||
|
}
|
||||||
|
const { email, id } = session.user
|
||||||
|
|
||||||
|
// Generate a random 4-digit code
|
||||||
|
const pin = Math.floor(Math.random() * 10000)
|
||||||
|
.toString()
|
||||||
|
.padStart(4, "0")
|
||||||
|
|
||||||
|
let created = false
|
||||||
|
|
||||||
|
let game = await getAnyRunningGame(id)
|
||||||
|
if (game) {
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
redirectUrl: "/api/game/running",
|
||||||
|
message: "Running game already exists.",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
game = await prisma.game.create({
|
||||||
|
data: {
|
||||||
|
gamePin: {
|
||||||
|
create: {
|
||||||
|
pin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
create: {
|
||||||
|
userId: id,
|
||||||
|
index: 1,
|
||||||
|
chats: {
|
||||||
|
create: {
|
||||||
|
event: "created",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = composeBody(game)
|
||||||
|
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
message: `User <${email}> created game: ${game.id}`,
|
||||||
|
statusCode: created ? 201 : 200,
|
||||||
|
body,
|
||||||
|
type: ["debug", "infoCyan"],
|
||||||
|
})
|
||||||
|
}
|
89
leaky-ships/pages/api/game/join.ts
Normal file
89
leaky-ships/pages/api/game/join.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { authOptions } from "../auth/[...nextauth]"
|
||||||
|
import running, { composeBody, gameSelects } from "./running"
|
||||||
|
import sendError from "@backend/sendError"
|
||||||
|
import sendResponse from "@backend/sendResponse"
|
||||||
|
import { rejectionErrors } from "@lib/backend/errors"
|
||||||
|
import getPinFromBody from "@lib/backend/getPinFromBody"
|
||||||
|
import logging from "@lib/backend/logging"
|
||||||
|
import prisma from "@lib/prisma"
|
||||||
|
import { GamePropsSchema } from "@lib/zodSchemas"
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
import { getServerSession } from "next-auth"
|
||||||
|
|
||||||
|
export default async function join(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<GamePropsSchema>
|
||||||
|
) {
|
||||||
|
const session = await getServerSession(req, res, authOptions)
|
||||||
|
const pin = await getPinFromBody(req, res)
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return sendResponse(req, res, rejectionErrors.unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email, id } = session.user
|
||||||
|
|
||||||
|
try {
|
||||||
|
const game = await prisma.game.findFirst({
|
||||||
|
where: {
|
||||||
|
gamePin: {
|
||||||
|
pin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!game) {
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
message: "Spiel existiert nicht",
|
||||||
|
statusCode: 404,
|
||||||
|
type: ["infoCyan"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const games = await prisma.game.findMany({
|
||||||
|
where: {
|
||||||
|
NOT: {
|
||||||
|
state: "ended",
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
some: {
|
||||||
|
userId: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
if (games.length) {
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
message: "Spieler ist bereits in Spiel!",
|
||||||
|
statusCode: 409,
|
||||||
|
type: ["infoCyan"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user_Game = await prisma.user_Game.create({
|
||||||
|
data: {
|
||||||
|
gameId: game.id,
|
||||||
|
userId: id,
|
||||||
|
index: 2,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
game: gameSelects,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = composeBody(user_Game.game)
|
||||||
|
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
message: `User <${email}> joined game: ${game.id}`,
|
||||||
|
body,
|
||||||
|
type: ["debug", "infoCyan"],
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
await logging(
|
||||||
|
"HERE".red + err.code + err.meta + err.message,
|
||||||
|
["error"],
|
||||||
|
req
|
||||||
|
)
|
||||||
|
throw sendError(req, res, rejectionErrors.gameNotFound)
|
||||||
|
}
|
||||||
|
}
|
129
leaky-ships/pages/api/game/running.ts
Normal file
129
leaky-ships/pages/api/game/running.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { authOptions } from "../auth/[...nextauth]"
|
||||||
|
import sendResponse from "@backend/sendResponse"
|
||||||
|
import { rejectionErrors } from "@lib/backend/errors"
|
||||||
|
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
|
||||||
|
import prisma from "@lib/prisma"
|
||||||
|
import { GamePropsSchema } from "@lib/zodSchemas"
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
import { getServerSession } from "next-auth"
|
||||||
|
|
||||||
|
export const gameSelects = {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
allowChat: true,
|
||||||
|
allowMarkDraw: true,
|
||||||
|
allowSpecials: true,
|
||||||
|
allowSpectators: true,
|
||||||
|
state: true,
|
||||||
|
gamePin: {
|
||||||
|
select: {
|
||||||
|
pin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
index: true,
|
||||||
|
chats: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
event: true,
|
||||||
|
message: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
moves: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAnyGame = (gameId: string) => {
|
||||||
|
const game = prisma.game.findFirst({
|
||||||
|
where: {
|
||||||
|
NOT: {
|
||||||
|
state: "ended",
|
||||||
|
},
|
||||||
|
id: gameId,
|
||||||
|
},
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
return game
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAnyRunningGame = (userId: string) => {
|
||||||
|
const game = prisma.game.findFirst({
|
||||||
|
where: {
|
||||||
|
NOT: {
|
||||||
|
state: "ended",
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
return game
|
||||||
|
}
|
||||||
|
|
||||||
|
export function composeBody(
|
||||||
|
gameDB: NonNullable<Awaited<ReturnType<typeof getAnyRunningGame>>>
|
||||||
|
): GamePropsSchema {
|
||||||
|
const { gamePin, ...game } = gameDB
|
||||||
|
const users = gameDB.users
|
||||||
|
.map(({ user, ...props }) => ({
|
||||||
|
...props,
|
||||||
|
...user,
|
||||||
|
}))
|
||||||
|
.sort((user1, user2) => user1.index - user2.index)
|
||||||
|
const payload = {
|
||||||
|
game: game,
|
||||||
|
gamePin: gamePin?.pin ?? null,
|
||||||
|
users,
|
||||||
|
}
|
||||||
|
return getPayloadwithChecksum(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function running(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<GamePropsSchema>
|
||||||
|
) {
|
||||||
|
const session = await getServerSession(req, res, authOptions)
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return sendResponse(req, res, rejectionErrors.unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email, id } = session.user
|
||||||
|
|
||||||
|
const game = await getAnyRunningGame(id)
|
||||||
|
|
||||||
|
if (!game)
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
message: `User <${email}> is in no game.`,
|
||||||
|
statusCode: 204,
|
||||||
|
type: ["debug", "infoCyan"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = composeBody(game)
|
||||||
|
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
message: `User <${email}> asked for game: ${game.id}`,
|
||||||
|
statusCode: 200,
|
||||||
|
body,
|
||||||
|
type: ["debug", "infoCyan"],
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,98 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import logging, { Logging } from "../../lib/backend/logging"
|
|
||||||
import getPlayerByNameDB from "../../lib/backend/components/getPlayerByNameDB"
|
|
||||||
import checkPasswordIsValid from "../../lib/backend/components/checkPasswordIsValid"
|
|
||||||
import createTokenDB from "../../lib/backend/components/createTokenDB"
|
|
||||||
import sendResponse from "../../lib/backend/components/sendResponse"
|
|
||||||
import sendError from "../../lib/backend/components/sendError"
|
|
||||||
import { setCookie } from "cookies-next"
|
|
||||||
import { Player, Token } from "@prisma/client"
|
|
||||||
import prisma from "../../lib/prisma"
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
loggedIn: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function login(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<any>
|
|
||||||
) {
|
|
||||||
const { username, password } = req.body
|
|
||||||
return preCheck({
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
newTokenType: "REFRESH" as Token["type"],
|
|
||||||
})
|
|
||||||
.then(getPlayerByNameDB)
|
|
||||||
.then(checkPasswordIsValid)
|
|
||||||
.then(createTokenDB)
|
|
||||||
.then(loginResponse<Data>)
|
|
||||||
.then(sendResponse<Data>)
|
|
||||||
.catch((err) => sendError(req, res, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function preCheck<T>(
|
|
||||||
payload: T & {
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<T>
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const { req } = payload
|
|
||||||
const oldRefreshToken = req.cookies.token
|
|
||||||
// Check for old cookie, if unused invalidate it
|
|
||||||
const oldDBToken = await prisma.token.findUnique({
|
|
||||||
where: {
|
|
||||||
token: oldRefreshToken,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (oldDBToken?.used) {
|
|
||||||
await prisma.token.update({
|
|
||||||
where: {
|
|
||||||
token: oldRefreshToken,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
used: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await logging("Old token has been invalidated.", ["debug"], req)
|
|
||||||
}
|
|
||||||
return { ...payload, noCookiePresent: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginResponse<T>(payload: {
|
|
||||||
player: Player
|
|
||||||
passwordIsValid: boolean
|
|
||||||
refreshToken: string
|
|
||||||
refreshTokenDB: Token
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<T>
|
|
||||||
}) {
|
|
||||||
const { player, refreshToken, refreshTokenDB, req, res } = payload
|
|
||||||
|
|
||||||
// Set login cookie
|
|
||||||
setCookie("token", refreshToken, {
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
maxAge: 172800000,
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: true,
|
|
||||||
secure: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Successfull response
|
|
||||||
return {
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
result: {
|
|
||||||
message:
|
|
||||||
"User " +
|
|
||||||
player.id +
|
|
||||||
" logged in and generated Refresh-Token: " +
|
|
||||||
refreshTokenDB.id,
|
|
||||||
body: { loggedIn: true },
|
|
||||||
type: ["debug", "info.cyan"] as Logging[],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import checkTokenIsValid from "../../lib/backend/components/checkTokenIsValid"
|
|
||||||
import sendResponse from "../../lib/backend/components/sendResponse"
|
|
||||||
import sendError from "../../lib/backend/components/sendError"
|
|
||||||
import { deleteCookie } from "cookies-next"
|
|
||||||
import { Token } from "@prisma/client"
|
|
||||||
import getTokenDB from "../../lib/backend/components/getTokenDB"
|
|
||||||
import getTokenFromCookie from "../../lib/backend/components/getTokenFromCookie"
|
|
||||||
import logging, { Logging } from "../../lib/backend/logging"
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
loggedOut: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function logout(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<any>
|
|
||||||
) {
|
|
||||||
return getTokenFromCookie({ req, res })
|
|
||||||
.then(checkTokenIsValid)
|
|
||||||
.then(getTokenDB)
|
|
||||||
.then(logoutResponse<Data>)
|
|
||||||
.then(sendResponse<Data>)
|
|
||||||
.catch((err) => sendError(req, res, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logoutResponse<T>(payload: {
|
|
||||||
tokenDB: Token
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<T>
|
|
||||||
}) {
|
|
||||||
const { tokenDB, req, res } = payload
|
|
||||||
|
|
||||||
// Set login cookie
|
|
||||||
deleteCookie("token", { req, res })
|
|
||||||
|
|
||||||
// Successfull response
|
|
||||||
return {
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
result: {
|
|
||||||
message: "User of Token " + tokenDB.id + " logged out.",
|
|
||||||
body: { loggedOut: true },
|
|
||||||
type: ["debug", "info.cyan"] as Logging[],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
import createPlayerDB from "../../lib/backend/components/createPlayerDB"
|
|
||||||
import sendError from "../../lib/backend/components/sendError"
|
|
||||||
import sendResponse from "../../lib/backend/components/sendResponse"
|
|
||||||
import { Logging } from "../../lib/backend/logging"
|
|
||||||
import { Player } from "@prisma/client"
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
registered: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function register(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<any>
|
|
||||||
) {
|
|
||||||
const { username, password } = req.body
|
|
||||||
return createPlayerDB({ req, res, username, password })
|
|
||||||
.then(registerResponse<Data>)
|
|
||||||
.then(sendResponse<Data>)
|
|
||||||
.catch((err) => sendError(req, res, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerResponse<T>(payload: {
|
|
||||||
player: Player
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<T>
|
|
||||||
}) {
|
|
||||||
const { player, req, res } = payload
|
|
||||||
|
|
||||||
// Successfull response
|
|
||||||
return {
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
result: {
|
|
||||||
message: "Player created : " + player.id,
|
|
||||||
statusCode: 201,
|
|
||||||
body: { registered: true },
|
|
||||||
type: ["debug", "info.cyan"] as Logging[],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +1,217 @@
|
||||||
import type { NextApiRequest } from "next"
|
import {
|
||||||
|
NextApiResponseWithSocket,
|
||||||
|
sServer,
|
||||||
|
} from "../../interfaces/NextApiSocket"
|
||||||
|
import {
|
||||||
|
composeBody,
|
||||||
|
gameSelects,
|
||||||
|
getAnyGame,
|
||||||
|
getAnyRunningGame,
|
||||||
|
} from "./game/running"
|
||||||
|
import logging from "@lib/backend/logging"
|
||||||
|
import prisma from "@lib/prisma"
|
||||||
|
import { GamePropsSchema } from "@lib/zodSchemas"
|
||||||
|
import colors from "colors"
|
||||||
|
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 { NextApiResponseWithSocket } from "../../interfaces/NextApiSocket"
|
|
||||||
|
|
||||||
const SocketHandler = (req: NextApiRequest, res: NextApiResponseWithSocket) => {
|
colors.enable()
|
||||||
|
|
||||||
|
const SocketHandler = async (
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponseWithSocket
|
||||||
|
) => {
|
||||||
if (res.socket.server.io) {
|
if (res.socket.server.io) {
|
||||||
console.log("Socket is already running " + req.url)
|
logging("Socket is already running " + req.url, ["infoCyan"], req)
|
||||||
} else {
|
} else {
|
||||||
console.log("Socket is initializing " + req.url)
|
logging("Socket is initializing " + req.url, ["infoCyan"], req)
|
||||||
const io = new Server(res.socket.server)
|
const io: sServer = new Server(res.socket.server, {
|
||||||
|
path: "/api/ws",
|
||||||
|
cors: {
|
||||||
|
origin: "https://leaky-ships.mal-noh.de",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
res.socket.server.io = io
|
res.socket.server.io = io
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
// io.use(authenticate)
|
||||||
socket.on("input-change", (msg) => {
|
io.use(async (socket, next) => {
|
||||||
socket.broadcast.emit("update-input", msg)
|
try {
|
||||||
})
|
const session = await getSession({
|
||||||
// console.log(socket.id)
|
req: socket.request,
|
||||||
// console.log(socket)
|
})
|
||||||
// ...
|
if (!session) return next(new Error(status["401"]))
|
||||||
|
socket.data.user = session.user
|
||||||
|
|
||||||
socket.on("test", (payload) => {
|
const game = await getAnyRunningGame(socket.data.user?.id ?? "")
|
||||||
console.log("Got test:", payload)
|
if (!game) {
|
||||||
// ...
|
logging(
|
||||||
|
"Forbidden, no game found: " +
|
||||||
|
JSON.stringify(Array.from(socket.rooms)),
|
||||||
|
["debug"],
|
||||||
|
socket.request
|
||||||
|
)
|
||||||
|
return next(new Error(status["403"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { payload, hash } = composeBody(game)
|
||||||
|
// let index: number | null = null
|
||||||
|
const index = payload.users.findIndex(
|
||||||
|
(user) => socket.data.user?.id === user?.id
|
||||||
|
)
|
||||||
|
if (index < 0) return next(new Error(status["401"]))
|
||||||
|
socket.data.index = index
|
||||||
|
socket.data.gameId = game.id
|
||||||
|
socket.join(game.id)
|
||||||
|
io.to(game.id).emit("playerEvent", {
|
||||||
|
type: "connect",
|
||||||
|
i: socket.data.index,
|
||||||
|
payload: { users: payload.users },
|
||||||
|
hash,
|
||||||
|
userId: socket.data.user?.id ?? "",
|
||||||
|
})
|
||||||
|
|
||||||
|
next()
|
||||||
|
} catch (err: any) {
|
||||||
|
logging("Unkonwn error - " + status["401"], ["warn"], socket.request)
|
||||||
|
next(new Error(status["401"]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
io.on("connection", async (socket) => {
|
||||||
|
logging(
|
||||||
|
`User connected <${socket.data.user?.email}>`.green +
|
||||||
|
", " +
|
||||||
|
socket.id.cyan,
|
||||||
|
["infoGreen"],
|
||||||
|
socket.request
|
||||||
|
)
|
||||||
|
|
||||||
|
socket.on("update", async (cb) => {
|
||||||
|
const game = await getAnyGame(socket.data.gameId ?? "")
|
||||||
|
if (!game) return
|
||||||
|
const body = composeBody(game)
|
||||||
|
cb(body)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.emit("test2", "lol")
|
socket.on("gameSetting", async (payload, cb) => {
|
||||||
|
const game = await prisma.game.update({
|
||||||
|
where: { id: socket.data.gameId ?? "" },
|
||||||
|
data: payload,
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
const { hash } = composeBody(game)
|
||||||
|
if (!hash) return
|
||||||
|
cb(hash)
|
||||||
|
io.to(game.id).emit(
|
||||||
|
"gameSetting",
|
||||||
|
payload,
|
||||||
|
hash,
|
||||||
|
socket.data.user?.id ?? ""
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("ping", (count, callback) => {
|
||||||
|
callback(count)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("leave", async (cb) => {
|
||||||
|
if (!socket.data.gameId || !socket.data.user?.id) return cb(false)
|
||||||
|
const user_Game = await prisma.user_Game.delete({
|
||||||
|
where: {
|
||||||
|
gameId_userId: {
|
||||||
|
gameId: socket.data.gameId,
|
||||||
|
userId: socket.data.user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const enemy = await prisma.user_Game.findFirst({
|
||||||
|
where: {
|
||||||
|
gameId: socket.data.gameId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
let body: GamePropsSchema
|
||||||
|
if (user_Game.index === 1 && enemy) {
|
||||||
|
const { game } = await prisma.user_Game.update({
|
||||||
|
where: {
|
||||||
|
gameId_index: {
|
||||||
|
gameId: socket.data.gameId,
|
||||||
|
index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
game: { ...gameSelects },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
body = composeBody(game)
|
||||||
|
} else {
|
||||||
|
const game = await prisma.game.findUnique({
|
||||||
|
where: {
|
||||||
|
id: socket.data.gameId,
|
||||||
|
},
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
if (!game) return cb(false)
|
||||||
|
body = composeBody(game)
|
||||||
|
}
|
||||||
|
const { payload, hash } = body
|
||||||
|
if (!payload || !hash || socket.data.index === undefined)
|
||||||
|
return cb(false)
|
||||||
|
io.to(socket.data.gameId).emit("playerEvent", {
|
||||||
|
type: "leave",
|
||||||
|
i: socket.data.index,
|
||||||
|
payload: { users: payload.users },
|
||||||
|
hash,
|
||||||
|
userId: socket.data.user?.id ?? "",
|
||||||
|
})
|
||||||
|
cb(true)
|
||||||
|
|
||||||
|
if (!payload.users.length) {
|
||||||
|
await prisma.game.delete({
|
||||||
|
where: {
|
||||||
|
id: socket.data.gameId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("isReady", async (isReady) => {
|
||||||
|
if (socket.data.index === undefined || !socket.data.gameId) return
|
||||||
|
io.to(socket.data.gameId).emit(
|
||||||
|
"isReady",
|
||||||
|
{ i: socket.data.index, isReady },
|
||||||
|
socket.data.user?.id ?? ""
|
||||||
|
)
|
||||||
|
io.to(socket.data.gameId).emit(
|
||||||
|
"isConnected",
|
||||||
|
{ i: socket.data.index, isConnected: true },
|
||||||
|
socket.data.user?.id ?? ""
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("disconnecting", async () => {
|
||||||
|
logging(
|
||||||
|
"Disconnecting: " + JSON.stringify(Array.from(socket.rooms)),
|
||||||
|
["debug"],
|
||||||
|
socket.request
|
||||||
|
)
|
||||||
|
if (socket.data.index === undefined || !socket.data.gameId) return
|
||||||
|
io.to(socket.data.gameId).emit("playerEvent", {
|
||||||
|
type: "disconnect",
|
||||||
|
i: socket.data.index,
|
||||||
|
userId: socket.data.user?.id ?? "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
// socket.rooms.size === 0
|
||||||
|
logging("Disconnect: " + socket.id, ["debug"], socket.request)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
res.end()
|
res.end()
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { faEye, faLeftLong } from "@fortawesome/pro-regular-svg-icons"
|
|
||||||
import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [heWantsToPlay, setHeWantsToPlay] = useState(false)
|
|
||||||
return (
|
|
||||||
<div id="box">
|
|
||||||
<button id="navExpand">
|
|
||||||
<img id="burgerMenu" src="/assets/burger-menu.png" alt="Burger Menu" />
|
|
||||||
</button>
|
|
||||||
<div id="shield">
|
|
||||||
<div id="width">
|
|
||||||
<h1>Leaky</h1>
|
|
||||||
<h1>Ships</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!heWantsToPlay ? (
|
|
||||||
<>
|
|
||||||
<div id="videoWrapper">
|
|
||||||
<FontAwesomeIcon icon={faCirclePlay} />
|
|
||||||
</div>
|
|
||||||
<button id="startButton" onClick={() => setHeWantsToPlay(true)}>
|
|
||||||
START
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div id="startBox">
|
|
||||||
<button id="back" onClick={() => setHeWantsToPlay(false)}>
|
|
||||||
<FontAwesomeIcon icon={faLeftLong} />
|
|
||||||
</button>
|
|
||||||
<div id="sameWidth">
|
|
||||||
<button className="optionButton">
|
|
||||||
<span>Raum erstellen</span>
|
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
|
||||||
</button>
|
|
||||||
<button className="optionButton">
|
|
||||||
<span>Raum beitreten</span>
|
|
||||||
<FontAwesomeIcon icon={faUserPlus} />
|
|
||||||
</button>
|
|
||||||
<button className="optionButton">
|
|
||||||
<span>Zuschauen</span>
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,256 +0,0 @@
|
||||||
import {
|
|
||||||
faToggleLargeOff,
|
|
||||||
faToggleLargeOn,
|
|
||||||
faXmark,
|
|
||||||
} from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
|
|
||||||
import classNames from "classnames"
|
|
||||||
import Head from "next/head"
|
|
||||||
import { Dispatch, SetStateAction, useEffect, useState } from "react"
|
|
||||||
|
|
||||||
const settingOptionsInit: { [key: string]: boolean } = {
|
|
||||||
allowSpectators: false,
|
|
||||||
allowSpecials: false,
|
|
||||||
allowChat: false,
|
|
||||||
allowMarkDraw: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [enemy, setEnemy] = useState(false)
|
|
||||||
const [settings, setSettings] = useState(false)
|
|
||||||
const [dots, setDots] = useState(0)
|
|
||||||
const [settingOptions, setSettingOptions] = useState(settingOptionsInit)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (enemy) return
|
|
||||||
const interval = setInterval(() => setDots((e) => (e % 3) + 1), 1000)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="box">
|
|
||||||
<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>
|
|
||||||
<button className="absolute top-16 left-16 flex h-24 w-24 items-center justify-center rounded-lg bg-grayish">
|
|
||||||
<img
|
|
||||||
className="pixelart h-20 w-20"
|
|
||||||
src="/assets/burger-menu.png"
|
|
||||||
alt="Burger Menu"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div className="flex flex-row bg-shield-gray bg-[url('/assets/shield.png')] bg-cover bg-no-repeat">
|
|
||||||
<div className="flex flex-col items-center justify-between">
|
|
||||||
<h1 className="font-checkpoint my-4 mx-32 border-y-4 border-slate-700 text-center text-6xl leading-tight tracking-widest">
|
|
||||||
Leaky
|
|
||||||
<br />
|
|
||||||
Ships
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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" text="Chat" />
|
|
||||||
<h1 className="font-farro text-5xl font-medium">
|
|
||||||
Game-PIN: <span className="underline">3169</span>
|
|
||||||
</h1>
|
|
||||||
<Icon
|
|
||||||
src="gear.png"
|
|
||||||
text="Settings"
|
|
||||||
onClick={() => setSettings(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-around">
|
|
||||||
<Player
|
|
||||||
src="player_blue.png"
|
|
||||||
text="Spieler 1 (Du)"
|
|
||||||
primary={true}
|
|
||||||
edit={true}
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
className="font-farro m-4 text-6xl font-semibold"
|
|
||||||
onClick={() => setEnemy((e) => !e)}
|
|
||||||
>
|
|
||||||
VS
|
|
||||||
</p>
|
|
||||||
{enemy ? (
|
|
||||||
<Player src="player_red.png" text="Spieler 2" />
|
|
||||||
) : (
|
|
||||||
<p className="font-farro m-12 w-64 text-center text-5xl font-medium">
|
|
||||||
Warte auf Spieler 2 {Array.from(Array(dots), () => ".").join("")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center border-t-2 border-slate-900">
|
|
||||||
<button className="font-farro m-8 rounded-xl bg-amber-400 px-12 py-4 text-5xl font-medium">
|
|
||||||
START
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{settings ? (
|
|
||||||
<Settings
|
|
||||||
props={{
|
|
||||||
settingOptions,
|
|
||||||
setSettingOptions,
|
|
||||||
closeSettings: () => setSettings(false),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Icon({
|
|
||||||
src,
|
|
||||||
text,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
src: string
|
|
||||||
text: string
|
|
||||||
onClick?: () => void
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="mx-4 mt-4 flex flex-col items-center border-none"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="pixelart mb-1 box-content w-16 rounded-xl bg-white p-1"
|
|
||||||
src={"/assets/" + src}
|
|
||||||
alt={src}
|
|
||||||
/>
|
|
||||||
<span className="font-semibold">{text}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function Player({
|
|
||||||
src,
|
|
||||||
text,
|
|
||||||
primary,
|
|
||||||
edit,
|
|
||||||
}: {
|
|
||||||
src: string
|
|
||||||
text: string
|
|
||||||
primary?: boolean
|
|
||||||
edit?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="m-4 flex flex-col items-center">
|
|
||||||
<p
|
|
||||||
className={classNames(
|
|
||||||
"font-farro my-8 text-5xl",
|
|
||||||
primary ? "font-semibold" : "font-normal"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</p>
|
|
||||||
<div className="relative m-8">
|
|
||||||
<img className="pixelart w-64" src={"/assets/" + src} alt={src} />
|
|
||||||
{edit ? (
|
|
||||||
<button className="absolute top-4 right-4 h-14 w-14 rounded-lg border-2 border-dashed border-warn bg-gray-800 bg-opacity-90">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="h-full w-full text-warn"
|
|
||||||
icon={faCaretDown}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function Settings({
|
|
||||||
props: { settingOptions, setSettingOptions, closeSettings },
|
|
||||||
}: {
|
|
||||||
props: {
|
|
||||||
settingOptions: typeof settingOptionsInit
|
|
||||||
setSettingOptions: Dispatch<SetStateAction<typeof settingOptionsInit>>
|
|
||||||
closeSettings: () => void
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="absolute inset-0 flex flex-col justify-center bg-black bg-opacity-40">
|
|
||||||
<div className="relative mx-16 flex flex-col rounded-3xl border-4 border-zinc-700 bg-zinc-400 p-8">
|
|
||||||
<h1 className="font-farro text-center text-6xl font-semibold text-white shadow-black drop-shadow-lg">
|
|
||||||
Settings
|
|
||||||
</h1>
|
|
||||||
<button
|
|
||||||
className="absolute top-6 right-6 h-14 w-14 "
|
|
||||||
onClick={closeSettings}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="h-full w-full text-gray-700 drop-shadow-md"
|
|
||||||
icon={faXmark}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div className="relative m-8 rounded-xl bg-zinc-500 pt-16">
|
|
||||||
<button
|
|
||||||
className="absolute top-8 right-12 h-14 w-14"
|
|
||||||
onClick={() => setSettingOptions(settingOptionsInit)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="h-full w-full text-gray-700 drop-shadow-md"
|
|
||||||
icon={faRotateLeft}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div className="mr-32 grid grid-cols-3 items-center justify-items-start gap-4 p-8">
|
|
||||||
{Object.keys(settingOptions).map((key) => {
|
|
||||||
const state = settingOptions[key]
|
|
||||||
const onClick = () =>
|
|
||||||
setSettingOptions((e) => ({ ...e, [key]: !e[key] }))
|
|
||||||
return <Setting key={key} props={{ key, state, onClick }} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function Setting({
|
|
||||||
props: { key, state, onClick },
|
|
||||||
}: {
|
|
||||||
props: {
|
|
||||||
key: string
|
|
||||||
state: boolean
|
|
||||||
onClick: () => void
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label
|
|
||||||
htmlFor={key}
|
|
||||||
className="col-span-2 my-4 select-none text-8xl text-white drop-shadow-md"
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
htmlFor={key}
|
|
||||||
className={state ? "rounded-full bg-gray-300 px-2 transition-all" : ""}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className={classNames(
|
|
||||||
"w-24 drop-shadow-md",
|
|
||||||
state ? "text-blue-500" : "text-gray-700"
|
|
||||||
)}
|
|
||||||
icon={state ? faToggleLargeOn : faToggleLargeOff}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="bg-none"
|
|
||||||
checked={state}
|
|
||||||
type="checkbox"
|
|
||||||
id={key}
|
|
||||||
onChange={onClick}
|
|
||||||
hidden={true}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { ChangeEventHandler, useEffect, useState } from "react"
|
|
||||||
import { io } from "socket.io-client"
|
|
||||||
let socket: ReturnType<typeof io>
|
|
||||||
|
|
||||||
const Home = () => {
|
|
||||||
const [input, setInput] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
socketInitializer()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const socketInitializer = async () => {
|
|
||||||
await fetch("/api/ws")
|
|
||||||
socket = io()
|
|
||||||
|
|
||||||
socket.on("connect", () => {
|
|
||||||
console.log("connected")
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on("update-input", (msg) => {
|
|
||||||
setInput(msg)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
setInput(e.target.value)
|
|
||||||
socket.emit("input-change", e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
placeholder="Type something"
|
|
||||||
value={input}
|
|
||||||
onChange={onChangeHandler}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home
|
|
|
@ -1,15 +0,0 @@
|
||||||
import SocketIO from "../../components/SocketIO"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main>
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<SocketIO />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
44
leaky-ships/pages/game.tsx
Normal file
44
leaky-ships/pages/game.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { useGameProps } from "@hooks/useGameProps"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import React, { 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>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
|
import Gamefield from "@components/Gamefield/Gamefield"
|
||||||
import Head from "next/head"
|
import Head from "next/head"
|
||||||
import Gamefield from "../../components/Gamefield"
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
|
@ -1,5 +1,5 @@
|
||||||
|
import Grid from "@components/Grid"
|
||||||
import Head from "next/head"
|
import Head from "next/head"
|
||||||
import Grid from "../../components/Grid"
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
|
@ -1,5 +1,5 @@
|
||||||
|
import Grid2 from "@components/Grid2"
|
||||||
import Head from "next/head"
|
import Head from "next/head"
|
||||||
import Grid2 from "../../components/Grid2"
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
|
@ -1,47 +1,34 @@
|
||||||
import Head from "next/head"
|
import BurgerMenu from "@components/BurgerMenu"
|
||||||
import Link from "next/link"
|
import Logo from "@components/Logo"
|
||||||
|
import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-full bg-theme">
|
||||||
<Head>
|
<div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
|
||||||
<title>Create Next App</title>
|
<Logo />
|
||||||
<meta name="description" content="Generated by create next app" />
|
<BurgerMenu />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<div className="flex h-36 w-64 items-center justify-center rounded-xl border-4 border-black bg-[#2227] sm:h-48 sm:w-96 md:h-72 md:w-[32rem] md:border-[6px] xl:h-[26rem] xl:w-[48rem]">
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<FontAwesomeIcon
|
||||||
</Head>
|
className="text-6xl sm:text-7xl md:text-8xl"
|
||||||
<main>
|
icon={faCirclePlay}
|
||||||
<p>
|
/>
|
||||||
<Link href="/dev/gamefield" target="_blank">
|
</div>
|
||||||
Gamefield
|
<button
|
||||||
</Link>
|
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"
|
||||||
</p>
|
onClick={() =>
|
||||||
<p>
|
setTimeout(() => {
|
||||||
<Link href="/dev" target="_blank">
|
router.push("/start")
|
||||||
Homepage
|
}, 200)
|
||||||
</Link>
|
}
|
||||||
</p>
|
>
|
||||||
<p>
|
START
|
||||||
<Link href="/dev/grid" target="_blank">
|
</button>
|
||||||
Grid Effect
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/grid2" target="_blank">
|
|
||||||
Grid Effect with Content
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/socket" target="_blank">
|
|
||||||
Socket
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/socketio" target="_blank">
|
|
||||||
SocketIO
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
39
leaky-ships/pages/lobby.tsx
Normal file
39
leaky-ships/pages/lobby.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import BurgerMenu from "@components/BurgerMenu"
|
||||||
|
import LobbyFrame from "@components/Lobby/LobbyFrame"
|
||||||
|
import Settings from "@components/Lobby/SettingsFrame/Settings"
|
||||||
|
import Logo from "@components/Logo"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import Head from "next/head"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
export default function Lobby() {
|
||||||
|
const [settings, setSettings] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full bg-theme">
|
||||||
|
<Head>
|
||||||
|
<title>Lobby</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" />
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/cpfont_ote/CP Font.otf"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mx-auto flex h-full max-w-screen-2xl flex-col items-center justify-evenly",
|
||||||
|
{ "blur-sm": settings }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Logo small={true} />
|
||||||
|
<LobbyFrame openSettings={() => setSettings(true)} />
|
||||||
|
</div>
|
||||||
|
<BurgerMenu blur={settings} />
|
||||||
|
{settings ? <Settings closeSettings={() => setSettings(false)} /> : <></>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
147
leaky-ships/pages/login.tsx
Normal file
147
leaky-ships/pages/login.tsx
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { faWifiExclamation } from "@fortawesome/pro-duotone-svg-icons"
|
||||||
|
import {
|
||||||
|
faArrowLeft,
|
||||||
|
faCheck,
|
||||||
|
faSpinnerThird,
|
||||||
|
faXmark,
|
||||||
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { FormEvent, useState } from "react"
|
||||||
|
|
||||||
|
enum ProcessStates {
|
||||||
|
"waiting",
|
||||||
|
"loading",
|
||||||
|
"success",
|
||||||
|
"wrong",
|
||||||
|
"error",
|
||||||
|
}
|
||||||
|
const messages: { [key in ProcessStates]: string } = {
|
||||||
|
[ProcessStates.waiting]: "Enter Login Details",
|
||||||
|
[ProcessStates.loading]: "Logging in...",
|
||||||
|
[ProcessStates.success]: "Done!",
|
||||||
|
[ProcessStates.wrong]: "Wrong username or password",
|
||||||
|
[ProcessStates.error]: "An error occurred!",
|
||||||
|
}
|
||||||
|
const icons = {
|
||||||
|
[ProcessStates.loading]: faSpinnerThird,
|
||||||
|
[ProcessStates.success]: faCheck,
|
||||||
|
[ProcessStates.wrong]: faXmark,
|
||||||
|
[ProcessStates.error]: faWifiExclamation,
|
||||||
|
}
|
||||||
|
const iconClasses = {
|
||||||
|
[ProcessStates.loading]: "animate-spin",
|
||||||
|
[ProcessStates.success]: "text-green-500",
|
||||||
|
[ProcessStates.wrong]: "text-red-500",
|
||||||
|
[ProcessStates.error]: "animate-pulse text-amber-500 !text-8xl",
|
||||||
|
}
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
|
||||||
|
const [state, setState] = useState<ProcessStates>(ProcessStates.waiting)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
|
const elem = () => {
|
||||||
|
if (state === ProcessStates.waiting)
|
||||||
|
return (
|
||||||
|
<form onSubmit={login}>
|
||||||
|
<div className="mb-4 text-lg">
|
||||||
|
<input
|
||||||
|
className="rounded-3xl border-none bg-blue-400 bg-opacity-50 px-6 py-2 text-center text-inherit placeholder-slate-200 shadow-lg outline-none backdrop-blur-md"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder="Username or email"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 text-lg">
|
||||||
|
<input
|
||||||
|
className="rounded-3xl border-none bg-blue-400 bg-opacity-50 px-6 py-2 text-center text-inherit placeholder-slate-200 shadow-lg outline-none backdrop-blur-md"
|
||||||
|
type="Password"
|
||||||
|
name="name"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex justify-center text-lg text-black">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-3xl bg-blue-400 bg-opacity-50 px-10 py-2 text-white shadow-xl backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center rounded-3xl bg-slate-800/50 px-16 py-8 shadow-lg drop-shadow-md">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className={classNames("text-6xl", iconClasses[state])}
|
||||||
|
icon={icons[state]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex justify-center text-lg text-black">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-3xl bg-blue-400 bg-opacity-50 px-10 py-2 text-white shadow-xl backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
|
||||||
|
onClick={() => {
|
||||||
|
setState(ProcessStates.waiting)
|
||||||
|
setError("")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="-ml-4 mr-4" icon={faArrowLeft} />
|
||||||
|
Return
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
setState(ProcessStates.loading)
|
||||||
|
await fetch("/api/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 200) setState(ProcessStates.success)
|
||||||
|
if (res.status === 401) setState(ProcessStates.wrong)
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
setState(ProcessStates.error)
|
||||||
|
setError(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
{error ? error : messages[state]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{elem()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
38
leaky-ships/pages/logout.tsx
Normal file
38
leaky-ships/pages/logout.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import React, { useEffect } from "react"
|
||||||
|
|
||||||
|
function Logout() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => router.push("/dev"), 2000)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
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">Logging out...</span>
|
||||||
|
</div>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="mx-24 my-16 animate-spin text-6xl"
|
||||||
|
icon={faSpinnerThird}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logout
|
217
leaky-ships/pages/start.tsx
Normal file
217
leaky-ships/pages/start.tsx
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
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 { toast } from "react-toastify"
|
||||||
|
import { Icons } 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 action = !pin ? "erstellt" : "angefragt"
|
||||||
|
const toastId = "pageLoad"
|
||||||
|
toast("Raum wird " + action, {
|
||||||
|
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">
|
||||||
|
<button
|
||||||
|
className="-mt-2 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl"
|
||||||
|
onClick={() =>
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/")
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLeftLong} />
|
||||||
|
</button>
|
||||||
|
<div className="flex flex-col items-center gap-6 sm:gap-12">
|
||||||
|
<OptionButton
|
||||||
|
action={() => gameFetch()}
|
||||||
|
icon={faPlus}
|
||||||
|
disabled={!session}
|
||||||
|
>
|
||||||
|
Raum erstellen
|
||||||
|
</OptionButton>
|
||||||
|
<OptionButton
|
||||||
|
action={() => {
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { q: "join" },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
icon={faUserPlus}
|
||||||
|
disabled={!session}
|
||||||
|
>
|
||||||
|
{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} />}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"Raum beitreten"
|
||||||
|
)}
|
||||||
|
</OptionButton>
|
||||||
|
<OptionButton
|
||||||
|
icon={faEye}
|
||||||
|
action={() => {
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { q: "watch" },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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} />}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"Zuschauen"
|
||||||
|
)}
|
||||||
|
</OptionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
2538
leaky-ships/pnpm-lock.yaml
generated
2538
leaky-ships/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
8
leaky-ships/prettier.config.js
Normal file
8
leaky-ships/prettier.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
semi: false,
|
||||||
|
plugins: [
|
||||||
|
"@trivago/prettier-plugin-sort-imports",
|
||||||
|
"prettier-plugin-tailwindcss", // MUST come last
|
||||||
|
],
|
||||||
|
pluginSearchDirs: false,
|
||||||
|
}
|
4579
leaky-ships/prisma/generated/zod/index.ts
Normal file
4579
leaky-ships/prisma/generated/zod/index.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,78 +1,130 @@
|
||||||
|
generator zod {
|
||||||
|
provider = "zod-prisma-types"
|
||||||
|
}
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "mongodb"
|
provider = "mysql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Player {
|
model Account {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(cuid())
|
||||||
uuid String @unique @default(uuid())
|
userId String @map("user_id")
|
||||||
createdAt DateTime @default(now())
|
type String
|
||||||
updatedAt DateTime @updatedAt
|
provider String
|
||||||
anonymous Boolean @default(true)
|
providerAccountId String @map("provider_account_id")
|
||||||
username String @unique @default("")
|
refresh_token String? @db.Text
|
||||||
email String @unique @default("")
|
access_token String? @db.Text
|
||||||
passwordHash String @unique @default("")
|
expires_at Int?
|
||||||
games Game[] @relation(fields: [gameIds], references: [id])
|
ext_expires_in Int?
|
||||||
gameIds String[] @db.ObjectId
|
token_type String?
|
||||||
tokens Token[]
|
scope String?
|
||||||
|
id_token String? @db.Text
|
||||||
|
session_state String?
|
||||||
|
oauth_token_secret String?
|
||||||
|
oauth_token String?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([provider, providerAccountId])
|
||||||
|
@@map("accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TokenType {
|
model Session {
|
||||||
REFRESH
|
id String @id @default(cuid())
|
||||||
ACCESS
|
sessionToken String @unique @map("session_token")
|
||||||
|
userId String @map("user_id")
|
||||||
|
expires DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Token {
|
model User {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
name String?
|
||||||
expires DateTime @updatedAt
|
email String? @unique
|
||||||
owner Player @relation(fields: [ownerId], references: [id])
|
emailVerified DateTime? @map("email_verified")
|
||||||
ownerId String @db.ObjectId
|
image String? @db.Text
|
||||||
token String @unique
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
type TokenType
|
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||||
used Boolean @default(false)
|
games User_Game[]
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
|
||||||
|
@@map(name: "users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String @unique
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@unique([identifier, token])
|
||||||
|
@@map("verificationtokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GameState {
|
||||||
|
launching
|
||||||
|
running
|
||||||
|
ended
|
||||||
}
|
}
|
||||||
|
|
||||||
model Game {
|
model Game {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
players Player[] @relation(fields: [playerIds], references: [id])
|
state GameState @default(launching)
|
||||||
playerIds String[] @db.ObjectId
|
allowSpectators Boolean @default(true)
|
||||||
running Boolean @default(true)
|
allowSpecials Boolean @default(true)
|
||||||
Move Move[]
|
allowChat Boolean @default(true)
|
||||||
Gamepin Gamepin[]
|
allowMarkDraw Boolean @default(true)
|
||||||
Chat Chat[]
|
gamePin Gamepin?
|
||||||
}
|
users User_Game[]
|
||||||
|
|
||||||
model Move {
|
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
// updatedAt DateTime @updatedAt
|
|
||||||
index Int
|
|
||||||
game Game @relation(fields: [gameId], references: [id])
|
|
||||||
gameId String @db.ObjectId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Gamepin {
|
model Gamepin {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
// updatedAt DateTime @updatedAt
|
pin String @unique
|
||||||
pin Int @unique
|
gameId String @unique
|
||||||
game Game @relation(fields: [gameId], references: [id])
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
gameId String @db.ObjectId
|
}
|
||||||
|
|
||||||
|
model User_Game {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
gameId String
|
||||||
|
userId String
|
||||||
|
index Int
|
||||||
|
moves Move[]
|
||||||
|
chats Chat[]
|
||||||
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@unique([gameId, index])
|
||||||
|
@@unique([gameId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Move {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
index Int
|
||||||
|
user_game_id String
|
||||||
|
user_game User_Game @relation(fields: [user_game_id], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([user_game_id, index])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Chat {
|
model Chat {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
// updatedAt DateTime @updatedAt
|
message String?
|
||||||
message String @default("")
|
event String?
|
||||||
event String @default("")
|
user_game_id String
|
||||||
game Game @relation(fields: [gameId], references: [id])
|
user_game User_Game @relation(fields: [user_game_id], references: [id], onDelete: Cascade)
|
||||||
gameId String @db.ObjectId
|
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
BIN
leaky-ships/public/images/wallpaper.jpg
Normal file
BIN
leaky-ships/public/images/wallpaper.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 MiB |
|
@ -71,6 +71,7 @@ body {
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
grid-template-rows: 0.75fr repeat(12, 1fr) 0.75fr;
|
grid-template-rows: 0.75fr repeat(12, 1fr) 0.75fr;
|
||||||
grid-template-columns: 0.75fr repeat(12, 1fr) 0.75fr;
|
grid-template-columns: 0.75fr repeat(12, 1fr) 0.75fr;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
grid-column: var(--x);
|
grid-column: var(--x);
|
||||||
|
@ -197,7 +198,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.preview {
|
&.preview {
|
||||||
--color: lawngreen;
|
--color: forestgreen;
|
||||||
background-color: #0001;
|
background-color: #0001;
|
||||||
// border: 5px dashed var(--color);
|
// border: 5px dashed var(--color);
|
||||||
}
|
}
|
||||||
|
@ -274,21 +275,70 @@ body {
|
||||||
width: inherit;
|
width: inherit;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&.left {
|
&:not(.middle) {
|
||||||
grid-area: 1 / 1 / -1 / -1;
|
grid-area: 1 / 1 / -1 / -1;
|
||||||
|
transform-origin: 324px 0;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
transform: rotate(180deg);
|
margin-top: 324px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
animation: floatInl 3s ease-out;
|
||||||
|
@keyframes floatInl {
|
||||||
|
from {
|
||||||
|
transform: scale(2) rotate(90deg);
|
||||||
|
margin-left: -324px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
grid-area: 1 / 1 / -1 / -1;
|
transform: rotate(270deg);
|
||||||
align-self: flex-end;
|
animation: floatInr 3s ease-out;
|
||||||
|
@keyframes floatInr {
|
||||||
|
from {
|
||||||
|
transform: scale(2) rotate(270deg);
|
||||||
|
margin-left: 324px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.top {
|
||||||
|
animation: floatInt 3s ease-out;
|
||||||
|
@keyframes floatInt {
|
||||||
|
from {
|
||||||
|
transform: scale(2);
|
||||||
|
margin-top: 648px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
animation: floatInb 3s ease-out;
|
||||||
|
@keyframes floatInb {
|
||||||
|
from {
|
||||||
|
transform: scale(2) rotate(180deg);
|
||||||
|
margin-top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.middle {
|
&.middle {
|
||||||
grid-area: 4 / 4 / -4 / -4;
|
grid-area: 4 / 4 / -4 / -4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
grid-area: 1 / 1 / -1 / -1;
|
||||||
|
border: 1px solid #000;
|
||||||
|
display: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-bar {
|
.event-bar {
|
||||||
|
@ -304,6 +354,22 @@ body {
|
||||||
width: 128px;
|
width: 128px;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
width: initial;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.amount::after {
|
||||||
|
content: var(--amount);
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -16px;
|
||||||
|
color: black;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -311,23 +377,6 @@ body {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.amount {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: var(--amount);
|
|
||||||
position: absolute;
|
|
||||||
top: -6px;
|
|
||||||
right: -6px;
|
|
||||||
color: black;
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Farro:wght@300;400;500;700&display=swap");
|
|
||||||
@font-face {
|
|
||||||
font-family: "CP_Font";
|
|
||||||
src: url("/fonts/cpfont_ote/CP Font.otf") format("opentype");
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixelart {
|
|
||||||
image-rendering: pixelated;
|
|
||||||
image-rendering: -moz-crisp-edges;
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-farro {
|
|
||||||
font-family: "Farro", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-checkpoint {
|
|
||||||
font-family: "CP_Font", sans-serif;
|
|
||||||
}
|
|
65
leaky-ships/styles/globals.scss
Normal file
65
leaky-ships/styles/globals.scss
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Farro:wght@300;400;500;700&display=swap");
|
||||||
|
@font-face {
|
||||||
|
font-family: "CP_Font";
|
||||||
|
src: url("/fonts/cpfont_ote/CP Font.otf") format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixelart {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-farro {
|
||||||
|
font-family: "Farro", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-checkpoint {
|
||||||
|
font-family: "CP_Font", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toast-confirm {
|
||||||
|
button {
|
||||||
|
margin: 0 1rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
border: 2px solid gray;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputStyle {
|
||||||
|
width: 3rem !important;
|
||||||
|
height: 3rem;
|
||||||
|
margin: 0 1rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b1b2b5cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer: fine) {
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,330 +0,0 @@
|
||||||
@use "./mixins/display" as *;
|
|
||||||
@use "./mixins/effects" as *;
|
|
||||||
@use "./mixins/CP_Font" as *;
|
|
||||||
@import "./mixins/variables";
|
|
||||||
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Farro:wght@300;400;500;700&display=swap");
|
|
||||||
|
|
||||||
#box {
|
|
||||||
min-height: 100%;
|
|
||||||
background-color: #282c34;
|
|
||||||
@include flex-col;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
|
|
||||||
#navExpand {
|
|
||||||
@include flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 64px;
|
|
||||||
left: 64px;
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
background-color: $grayish;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 2px 2px #0008 inset;
|
|
||||||
|
|
||||||
#burgerMenu {
|
|
||||||
@include pixelart;
|
|
||||||
height: 84px;
|
|
||||||
width: 84px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#shield {
|
|
||||||
@include flex-row;
|
|
||||||
justify-content: center;
|
|
||||||
height: 250px;
|
|
||||||
width: 700px;
|
|
||||||
background-image: url("/assets/shield.png");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
|
|
||||||
#width {
|
|
||||||
@include flex-col;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
@include CP_Font;
|
|
||||||
margin: 3%;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 5.8em;
|
|
||||||
letter-spacing: 6px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: 5px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 5px solid black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#videoWrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 350px;
|
|
||||||
width: 700px;
|
|
||||||
background-color: #2227;
|
|
||||||
border: 4px solid black;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 6em;
|
|
||||||
color: #231f20;
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke: black;
|
|
||||||
stroke-width: 2px;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#startButton {
|
|
||||||
font-family: "Farro", sans-serif;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 3em;
|
|
||||||
color: black;
|
|
||||||
background-color: $warn;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: calc(2rem + 8px) 6rem 2rem 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#startBox {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
background-color: $grayish;
|
|
||||||
box-shadow: 0 0 2px 2px #fffb inset, 0 0 2px 2px #fff2;
|
|
||||||
border: 4px solid black;
|
|
||||||
padding: 3rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-width: 60vw;
|
|
||||||
gap: 2rem;
|
|
||||||
|
|
||||||
#back {
|
|
||||||
font-size: 3em;
|
|
||||||
color: $grayish;
|
|
||||||
align-self: flex-start;
|
|
||||||
background-color: #000c;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
margin-top: -1.5rem;
|
|
||||||
width: 10rem;
|
|
||||||
box-shadow: 0 0 2px 2px #fff6 inset;
|
|
||||||
border: 2px solid #000c;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sameWidth {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3rem;
|
|
||||||
|
|
||||||
.optionButton {
|
|
||||||
@include flex-row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 2.5em;
|
|
||||||
color: $grayish;
|
|
||||||
background-color: #000c;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem 2rem 1rem 4rem;
|
|
||||||
width: 100%;
|
|
||||||
box-shadow: 0 0 2px 2px #fff6 inset;
|
|
||||||
border: 2px solid #000c;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-left: 3rem;
|
|
||||||
font-size: 2em;
|
|
||||||
width: 2.5rem;
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 2000px) {
|
|
||||||
#box {
|
|
||||||
#shield {
|
|
||||||
height: 200px;
|
|
||||||
width: 560px;
|
|
||||||
|
|
||||||
#width {
|
|
||||||
h1 {
|
|
||||||
font-size: 4.5em;
|
|
||||||
letter-spacing: 4px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: 4px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 4px solid black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
#box {
|
|
||||||
#navExpand {
|
|
||||||
top: 32px;
|
|
||||||
left: 32px;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
|
|
||||||
#burgerMenu {
|
|
||||||
height: 64px;
|
|
||||||
width: 64px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#shield {
|
|
||||||
height: 160px;
|
|
||||||
width: 450px;
|
|
||||||
|
|
||||||
#width {
|
|
||||||
h1 {
|
|
||||||
font-size: 3.6em;
|
|
||||||
letter-spacing: 3px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: 3px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 3px solid black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#videoWrapper {
|
|
||||||
height: 300px;
|
|
||||||
width: 600px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
#box {
|
|
||||||
justify-content: space-evenly;
|
|
||||||
|
|
||||||
#navExpand {
|
|
||||||
top: 16px;
|
|
||||||
left: 16px;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
|
|
||||||
#burgerMenu {
|
|
||||||
height: 44px;
|
|
||||||
width: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#shield {
|
|
||||||
height: 100px;
|
|
||||||
width: 280px;
|
|
||||||
|
|
||||||
#width {
|
|
||||||
h1 {
|
|
||||||
font-size: 2.2em;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: 2px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 2px solid black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#videoWrapper {
|
|
||||||
height: 250px;
|
|
||||||
width: 450px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 4em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#startButton {
|
|
||||||
font-size: 2em;
|
|
||||||
padding: calc(1rem + 8px) 3rem 1rem 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#startBox {
|
|
||||||
max-width: 90vw;
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
#back {
|
|
||||||
margin-top: -1rem;
|
|
||||||
font-size: 2em;
|
|
||||||
width: 7rem;
|
|
||||||
padding: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sameWidth {
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
.optionButton {
|
|
||||||
font-size: 2em;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-left: 2rem;
|
|
||||||
font-size: 1.5em;
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
#box {
|
|
||||||
#videoWrapper {
|
|
||||||
height: 150px;
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#startBox {
|
|
||||||
#sameWidth {
|
|
||||||
.optionButton {
|
|
||||||
font-size: 1.5em;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-left: 1rem;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
@font-face {
|
|
||||||
font-family: "CP_Font";
|
|
||||||
src: url("/fonts/cpfont_ote/CP Font.otf") format("opentype");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin CP_Font {
|
|
||||||
font-family: "CP_Font", sans-serif;
|
|
||||||
}
|
|
|
@ -1,21 +1,4 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
const allColors = require("tailwindcss/colors")
|
|
||||||
|
|
||||||
const colors = Object.keys(allColors)
|
|
||||||
.filter((key) => {
|
|
||||||
const deprecated = [
|
|
||||||
"lightBlue",
|
|
||||||
"warmGray",
|
|
||||||
"trueGray",
|
|
||||||
"coolGray",
|
|
||||||
"blueGray",
|
|
||||||
]
|
|
||||||
return !deprecated.find((val) => val === key)
|
|
||||||
})
|
|
||||||
.reduce((acc, key) => {
|
|
||||||
acc[key] = allColors[key]
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
|
@ -23,15 +6,16 @@ module.exports = {
|
||||||
"./components/**/*.{js,ts,jsx,tsx}",
|
"./components/**/*.{js,ts,jsx,tsx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
extend: {
|
||||||
...colors,
|
colors: {
|
||||||
theme: "#282c34",
|
theme: "#282c34",
|
||||||
grayish: "#b1b2b5cc",
|
grayish: "#b1b2b5cc",
|
||||||
warn: "#fabd04",
|
warn: "#fabd04",
|
||||||
"shield-gray": "#616161",
|
voidDark: "#000c",
|
||||||
"shield-lightgray": "#989898",
|
"shield-gray": "#616161",
|
||||||
|
"shield-lightgray": "#989898",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extend: {},
|
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,13 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"paths": {
|
||||||
|
"@lib/*": ["./lib/*"],
|
||||||
|
"@hooks/*": ["./hooks/*"],
|
||||||
|
"@components/*": ["./components/*"],
|
||||||
|
"@backend/*": ["./lib/backend/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|
12
leaky-ships/types/next-auth.d.ts
vendored
Normal file
12
leaky-ships/types/next-auth.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import NextAuth, { DefaultSession } from "next-auth"
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
/**
|
||||||
|
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||||
|
*/
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
} & DefaultSession["user"]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue