Compare commits
116 commits
Author | SHA1 | Date | |
---|---|---|---|
6fb057a102 | |||
777b807225 | |||
44bf98e88e | |||
8b179b5e65 | |||
2991dc19f2 | |||
c8a5c47b98 | |||
d245009c37 | |||
efcb61b1ed | |||
252f6f6028 | |||
4390269ed1 | |||
1e7b46ff69 | |||
a3eab18535 | |||
129e36a6f2 | |||
8e4c11570a | |||
26ee9652e6 | |||
b067747d48 | |||
b4fd992611 | |||
89b79fa245 | |||
db7fb9213e | |||
1cc34744a8 | |||
4c2c578311 | |||
2479dd518d | |||
ed472e40ed | |||
9d8bb8e20b | |||
fc7bf96b04 | |||
16a3279e5a | |||
3adddef8cc | |||
afe1e0426c | |||
6e9485df22 | |||
a95c0b1f7f | |||
7f350393ff | |||
eb8aee090f | |||
b407553f0d | |||
833130628e | |||
4d54a8b1f3 | |||
27fefef081 | |||
4020ca40f5 | |||
ce328cd1cd | |||
c71d74dd18 | |||
0291e6656e | |||
dbb9bd8476 | |||
0a6fd88733 | |||
e0e0f0a728 | |||
d9e0250b8c | |||
0b8fb0a476 | |||
c2af2dffa2 | |||
0317a3343c | |||
53a07b21b0 | |||
9895a286a3 | |||
787a85e425 | |||
84b4025bf9 | |||
f8fe083b03 | |||
63fc8c56bf | |||
29cb4a279d | |||
df315df8f4 | |||
4fa3b9ae64 | |||
1b05d1120b | |||
a088bff7be | |||
6bff62c0c2 | |||
d54db32980 | |||
b3695916a0 | |||
986555a368 | |||
dacc33bf21 | |||
f1ea064d4c | |||
61ae4b901d | |||
12295b316f | |||
30db96a3f7 | |||
a227da52bc | |||
2ed943857e | |||
f97b3a622a | |||
ba7097207c | |||
4af85bb572 | |||
207cf47c10 | |||
da53662d6e | |||
d3e8a43b34 | |||
c895bcef19 | |||
dab3abdda2 | |||
b9e72b2a32 | |||
549d0f1d77 | |||
2862f94f1c | |||
a32b40395e | |||
06f0eacc78 | |||
8cf563ed92 | |||
37e4f35e33 | |||
85fb7cfa7a | |||
4b33cff0d8 | |||
47ed7da260 | |||
051af9a147 | |||
ac578e9247 | |||
2a009585f4 | |||
8193b85274 | |||
39ddf8fde9 | |||
a8e5b57363 | |||
295acb1c84 | |||
bda0483b3d | |||
eb09017dab | |||
53d4ab527a | |||
4fae4b40b6 | |||
81c620f191 | |||
a8dbb30795 | |||
9e1522426a | |||
945ae9d1ac | |||
db79dc43c9 | |||
d825a0347f | |||
2ae2d51629 | |||
a61b423609 | |||
da9337a50b | |||
15296f9163 | |||
d86bf88e55 | |||
38f6d5eca9 | |||
d2594f51d5 | |||
41d152f0e3 | |||
9ce74a7227 | |||
b486ab99c2 | |||
3c6acb5597 | |||
78eabd3a7e |
66
.github/workflows/playwright.yml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: [self-hosted, linux]
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./leaky-ships
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
version: 8.7.4
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
name: Setup pnpm cache
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('./leaky-ships/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Add FA token
|
||||||
|
run: |
|
||||||
|
npm config set "@fortawesome:registry" "https://npm.fontawesome.com/"
|
||||||
|
npm config set "//npm.fontawesome.com/:_authToken" "${{ secrets.FONTAWESOME_NPM_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: pnpm playwright install --with-deps
|
||||||
|
|
||||||
|
- name: 'Compiling page'
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.ENV_FILE }}" > .env
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: pnpm playwright test
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: leaky-ships/playwright-report/
|
||||||
|
retention-days: 30
|
|
@ -1,5 +1,6 @@
|
||||||
# leaky-ships
|
# leaky-ships
|
||||||
Battleship web app with react frontend and ASP.NET Core backend
|
|
||||||
|
Battleship web app made with SolidJS using solid-start.
|
||||||
|
|
||||||
## Bluetooth
|
## Bluetooth
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals", "prettier"],
|
"parser": "@typescript-eslint/parser",
|
||||||
"rules": {
|
"env": {
|
||||||
"@next/next/no-img-element": "off"
|
"node": true
|
||||||
}
|
},
|
||||||
|
"plugins": ["solid"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:solid/typescript"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
56
leaky-ships/.gitignore
vendored
|
@ -1,39 +1,35 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
#logs
|
|
||||||
/log
|
/log
|
||||||
|
src/drizzle/migrations
|
||||||
|
|
||||||
|
dist
|
||||||
|
.vinxi
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
netlify
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
# IDEs and editors
|
||||||
/coverage
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
|
||||||
# next.js
|
# Temp
|
||||||
/.next/
|
gitignore
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
# System Files
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
Thumbs.db
|
||||||
|
|
||||||
# debug
|
# playwright
|
||||||
npm-debug.log*
|
/test-results/
|
||||||
yarn-debug.log*
|
/playwright-report/
|
||||||
yarn-error.log*
|
/playwright/.cache/
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
|
||||||
"pluginSearchDirs": false
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
function BurgerMenu() {
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BurgerMenu
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { CSSProperties, Dispatch, SetStateAction } from "react"
|
|
||||||
import { borderCN, cornerCN, fieldIndex } from "../../lib/utils/helpers"
|
|
||||||
import { Position, MouseCursor } from "../../interfaces/frontend"
|
|
||||||
|
|
||||||
type TilesType = {
|
|
||||||
key: number
|
|
||||||
isGameTile: boolean
|
|
||||||
classNameString: string
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function BorderTiles({
|
|
||||||
props: { count, settingTarget, setMouseCursor, setLastLeftTile },
|
|
||||||
}: {
|
|
||||||
props: {
|
|
||||||
count: number
|
|
||||||
settingTarget: (isGameTile: boolean, x: number, y: number) => void
|
|
||||||
setMouseCursor: Dispatch<SetStateAction<MouseCursor>>
|
|
||||||
setLastLeftTile: Dispatch<SetStateAction<Position>>
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
let tilesProperties: TilesType[] = []
|
|
||||||
|
|
||||||
for (let y = 0; y < count + 2; y++) {
|
|
||||||
for (let x = 0; x < count + 2; x++) {
|
|
||||||
const key = fieldIndex(count, x, y)
|
|
||||||
const cornerReslt = cornerCN(count, x, y)
|
|
||||||
const borderType = cornerReslt ? cornerReslt : borderCN(count, x, y)
|
|
||||||
const isGameTile = x > 0 && x < count + 1 && y > 0 && y < count + 1
|
|
||||||
const classNames = ["border-tile"]
|
|
||||||
if (borderType) classNames.push("edge", borderType)
|
|
||||||
if (isGameTile) classNames.push("game-tile")
|
|
||||||
const classNameString = classNames.join(" ")
|
|
||||||
tilesProperties.push({
|
|
||||||
key,
|
|
||||||
classNameString,
|
|
||||||
isGameTile,
|
|
||||||
x: x + 1,
|
|
||||||
y: y + 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{tilesProperties.map(({ key, classNameString, isGameTile, x, y }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
className={classNameString}
|
|
||||||
style={{ "--x": x, "--y": y } as CSSProperties}
|
|
||||||
onClick={() => settingTarget(isGameTile, x, y)}
|
|
||||||
onMouseEnter={() =>
|
|
||||||
setMouseCursor({ x, y, shouldShow: isGameTile })
|
|
||||||
}
|
|
||||||
onMouseLeave={() => setLastLeftTile({ x, y })}
|
|
||||||
></div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BorderTiles
|
|
|
@ -1,38 +0,0 @@
|
||||||
import React, { Dispatch, SetStateAction } from "react"
|
|
||||||
import { Items, Target } from "../../interfaces/frontend"
|
|
||||||
import Item from "./Item"
|
|
||||||
|
|
||||||
function EventBar({
|
|
||||||
props: { setMode, setTarget },
|
|
||||||
}: {
|
|
||||||
props: {
|
|
||||||
setMode: Dispatch<SetStateAction<number>>
|
|
||||||
setTarget: Dispatch<SetStateAction<Target>>
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
const items: Items[] = [
|
|
||||||
{ icon: "burger-menu", text: "Menu" },
|
|
||||||
{ icon: "radar", text: "Radar scan", mode: 0, amount: 1 },
|
|
||||||
{ icon: "torpedo", text: "Fire torpedo", mode: 1, amount: 1 },
|
|
||||||
{ icon: "scope", text: "Fire missile", mode: 2 },
|
|
||||||
{ icon: "gear", text: "Settings" },
|
|
||||||
]
|
|
||||||
return (
|
|
||||||
<div className="event-bar">
|
|
||||||
{items.map((e, i) => (
|
|
||||||
<Item
|
|
||||||
key={i}
|
|
||||||
props={{
|
|
||||||
...e,
|
|
||||||
callback: () => {
|
|
||||||
if (e.mode !== undefined) setMode(e.mode)
|
|
||||||
setTarget((e) => ({ ...e, show: false }))
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EventBar
|
|
|
@ -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,39 +0,0 @@
|
||||||
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 { Target, TargetList } from "../../interfaces/frontend"
|
|
||||||
|
|
||||||
export interface PointerProps extends Target, TargetList {
|
|
||||||
imply: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function GamefieldPointer({
|
|
||||||
props: { preview, x, y, show, type, edges, imply },
|
|
||||||
}: {
|
|
||||||
props: PointerProps
|
|
||||||
}) {
|
|
||||||
const isRadar = type === "radar"
|
|
||||||
const style = !(isRadar && !edges.filter((s) => s).length)
|
|
||||||
? { "--x": x, "--y": y }
|
|
||||||
: { "--x1": x - 1, "--x2": x + 2, "--y1": y - 1, "--y2": y + 2 }
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"hit-svg",
|
|
||||||
{ preview: preview },
|
|
||||||
"target",
|
|
||||||
type,
|
|
||||||
{ show: show },
|
|
||||||
...edges,
|
|
||||||
{ imply: imply }
|
|
||||||
)}
|
|
||||||
style={style as CSSProperties}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={!isRadar ? faCrosshairs : faRadar} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GamefieldPointer
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { CSSProperties } from "react"
|
|
||||||
import { Hit } from "../../interfaces/frontend"
|
|
||||||
|
|
||||||
function HitElems({ hits }: { hits: Hit[] }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{hits.map(({ hit, x, y }, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="hit-svg"
|
|
||||||
style={{ "--x": x, "--y": y } as CSSProperties}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={hit ? faBurst : faXmark} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HitElems
|
|
|
@ -1,33 +0,0 @@
|
||||||
import classNames from "classnames"
|
|
||||||
import React, { CSSProperties } from "react"
|
|
||||||
|
|
||||||
function Item({
|
|
||||||
props: { icon, text, amount, callback },
|
|
||||||
}: {
|
|
||||||
props: {
|
|
||||||
icon: string
|
|
||||||
text: string
|
|
||||||
amount?: number
|
|
||||||
callback: () => void
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="item" onClick={callback}>
|
|
||||||
<div
|
|
||||||
className={classNames("container", { amount: amount })}
|
|
||||||
style={
|
|
||||||
amount
|
|
||||||
? ({
|
|
||||||
"--amount": JSON.stringify(amount.toString()),
|
|
||||||
} as CSSProperties)
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img src={`/assets/${icon}.png`} alt={`${icon}.png`} />
|
|
||||||
</div>
|
|
||||||
<span>{text}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Item
|
|
|
@ -1,34 +0,0 @@
|
||||||
import classNames from "classnames"
|
|
||||||
import { CSSProperties } from "react"
|
|
||||||
|
|
||||||
function Ships() {
|
|
||||||
let shipIndexes = [
|
|
||||||
{ size: 2, index: null },
|
|
||||||
{ size: 3, index: 1 },
|
|
||||||
{ size: 3, index: 2 },
|
|
||||||
{ size: 3, index: 3 },
|
|
||||||
{ size: 4, index: 1 },
|
|
||||||
{ size: 4, index: 2 },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{shipIndexes.map(({ size, index }, i) => {
|
|
||||||
const filename = `/assets/ship_blue_${size}x${
|
|
||||||
index ? "_" + index : ""
|
|
||||||
}.gif`
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={classNames("ship", "s" + size)}
|
|
||||||
style={{ "--x": i + 3 } as CSSProperties}
|
|
||||||
>
|
|
||||||
<img src={filename} alt={filename} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Ships
|
|
|
@ -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
|
|
|
@ -1,112 +0,0 @@
|
||||||
import classNames from "classnames"
|
|
||||||
import { CSSProperties, useEffect, useMemo, useState } from "react"
|
|
||||||
|
|
||||||
function Grid() {
|
|
||||||
function floorClient(number: number) {
|
|
||||||
return Math.floor(number / 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [columns, setColumns] = useState(0)
|
|
||||||
const [rows, setRows] = useState(0)
|
|
||||||
const [params, setParams] = useState({
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
quantity: columns * rows,
|
|
||||||
})
|
|
||||||
const [position, setPosition] = useState([0, 0])
|
|
||||||
const [active, setActve] = useState(false)
|
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleResize() {
|
|
||||||
setColumns(floorClient(document.body.clientWidth))
|
|
||||||
setRows(floorClient(document.body.clientHeight))
|
|
||||||
}
|
|
||||||
handleResize()
|
|
||||||
window.addEventListener("resize", handleResize)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setParams({ columns, rows, quantity: columns * rows })
|
|
||||||
}, 500)
|
|
||||||
return () => clearTimeout(timeout)
|
|
||||||
}, [columns, rows])
|
|
||||||
|
|
||||||
const createTiles = useMemo(() => {
|
|
||||||
const colors = [
|
|
||||||
"rgb(229, 57, 53)",
|
|
||||||
"rgb(253, 216, 53)",
|
|
||||||
"rgb(244, 81, 30)",
|
|
||||||
"rgb(76, 175, 80)",
|
|
||||||
"rgb(33, 150, 243)",
|
|
||||||
"rgb(156, 39, 176)",
|
|
||||||
]
|
|
||||||
|
|
||||||
function createTile(index: number) {
|
|
||||||
const x = index % params.columns
|
|
||||||
const y = Math.floor(index / params.columns)
|
|
||||||
const xDiff = (x - position[0]) / 20
|
|
||||||
const yDiff = (y - position[1]) / 20
|
|
||||||
const pos = Math.sqrt(xDiff * xDiff + yDiff * yDiff).toFixed(2)
|
|
||||||
|
|
||||||
function doEffect(posX: number, posY: number) {
|
|
||||||
if (active) return
|
|
||||||
setPosition([posX, posY])
|
|
||||||
setActve(true)
|
|
||||||
|
|
||||||
function xDiff(x: number) {
|
|
||||||
return (x - posX) / 20
|
|
||||||
}
|
|
||||||
function yDiff(y: number) {
|
|
||||||
return (y - posY) / 20
|
|
||||||
}
|
|
||||||
function pos(x: number, y: number) {
|
|
||||||
return Math.sqrt(xDiff(x) * xDiff(x) + yDiff(y) * yDiff(y))
|
|
||||||
}
|
|
||||||
const diagonals = [
|
|
||||||
pos(0, 0),
|
|
||||||
pos(params.columns, 0),
|
|
||||||
pos(0, params.rows),
|
|
||||||
pos(params.columns, params.rows),
|
|
||||||
]
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setActve(false)
|
|
||||||
setCount((e) => e + 1)
|
|
||||||
}, Math.max(...diagonals) * 1000 + 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={classNames("tile", { active: active })}
|
|
||||||
style={{ "--delay": pos + "s" } as CSSProperties}
|
|
||||||
onClick={() => doEffect(x, y)}
|
|
||||||
></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="tiles"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--columns": params.columns,
|
|
||||||
"--rows": params.rows,
|
|
||||||
"--bg-color-1": colors[count % colors.length],
|
|
||||||
"--bg-color-2": colors[(count + 1) % colors.length],
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Array.from(Array(params.quantity), (_tile, index) =>
|
|
||||||
createTile(index)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}, [params, position, active, count])
|
|
||||||
|
|
||||||
return createTiles
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Grid
|
|
|
@ -1,118 +0,0 @@
|
||||||
import classNames from "classnames"
|
|
||||||
import { CSSProperties, useEffect, useMemo, useState } from "react"
|
|
||||||
|
|
||||||
function Grid2() {
|
|
||||||
function floorClient(number: number) {
|
|
||||||
return Math.floor(number / 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [columns, setColumns] = useState(0)
|
|
||||||
const [rows, setRows] = useState(0)
|
|
||||||
const [params, setParams] = useState({
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
quantity: columns * rows,
|
|
||||||
})
|
|
||||||
const [position, setPosition] = useState([0, 0])
|
|
||||||
const [active, setActve] = useState(false)
|
|
||||||
const [action, setAction] = useState(false)
|
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleResize() {
|
|
||||||
setColumns(floorClient(document.body.clientWidth))
|
|
||||||
setRows(floorClient(document.body.clientHeight))
|
|
||||||
}
|
|
||||||
handleResize()
|
|
||||||
window.addEventListener("resize", handleResize)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setParams({ columns, rows, quantity: columns * rows })
|
|
||||||
}, 500)
|
|
||||||
return () => clearTimeout(timeout)
|
|
||||||
}, [columns, rows])
|
|
||||||
|
|
||||||
const createTiles = useMemo(() => {
|
|
||||||
const sentences = [
|
|
||||||
"Ethem ...",
|
|
||||||
"hat ...",
|
|
||||||
"lange ...",
|
|
||||||
"Hörner 🐂",
|
|
||||||
"Grüße von Mallorca 🌊 🦦 ☀️",
|
|
||||||
]
|
|
||||||
|
|
||||||
function createTile(index: number) {
|
|
||||||
const x = index % params.columns
|
|
||||||
const y = Math.floor(index / params.columns)
|
|
||||||
const xDiff = (x - position[0]) / 20
|
|
||||||
const yDiff = (y - position[1]) / 20
|
|
||||||
const pos = Math.sqrt(xDiff * xDiff + yDiff * yDiff).toFixed(2)
|
|
||||||
|
|
||||||
function doEffect(posX: number, posY: number) {
|
|
||||||
if (action) return
|
|
||||||
setPosition([posX, posY])
|
|
||||||
setActve((e) => !e)
|
|
||||||
setAction(true)
|
|
||||||
|
|
||||||
function xDiff(x: number) {
|
|
||||||
return (x - posX) / 20
|
|
||||||
}
|
|
||||||
function yDiff(y: number) {
|
|
||||||
return (y - posY) / 20
|
|
||||||
}
|
|
||||||
function pos(x: number, y: number) {
|
|
||||||
return Math.sqrt(xDiff(x) * xDiff(x) + yDiff(y) * yDiff(y))
|
|
||||||
}
|
|
||||||
const diagonals = [
|
|
||||||
pos(0, 0),
|
|
||||||
pos(params.columns, 0),
|
|
||||||
pos(0, params.rows),
|
|
||||||
pos(params.columns, params.rows),
|
|
||||||
]
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setAction(false)
|
|
||||||
if (active) setCount((e) => e + 1)
|
|
||||||
}, Math.max(...diagonals) * 1000 + 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={classNames("tile", active ? "active" : "inactive")}
|
|
||||||
style={{ "--delay": pos + "s" } as CSSProperties}
|
|
||||||
onClick={() => doEffect(x, y)}
|
|
||||||
></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="tiles"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--columns": params.columns,
|
|
||||||
"--rows": params.rows,
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="center-div">
|
|
||||||
<h1
|
|
||||||
className={classNames("headline", !active ? "active" : "inactive")}
|
|
||||||
>
|
|
||||||
{sentences[count % sentences.length]}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
{Array.from(Array(params.quantity), (_tile, index) =>
|
|
||||||
createTile(index)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}, [params, position, active, action, count])
|
|
||||||
|
|
||||||
return createTiles
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Grid2
|
|
|
@ -1,27 +0,0 @@
|
||||||
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
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import Settings from "./SettingsFrame/Settings"
|
|
||||||
import Icon from "./Icon"
|
|
||||||
import Player from "./Player"
|
|
||||||
|
|
||||||
function LobbyFrame() {
|
|
||||||
const [settings, setSettings] = useState(false)
|
|
||||||
const [enemy, setEnemy] = useState(false)
|
|
||||||
const [dots, setDots] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (enemy) return
|
|
||||||
const interval = setInterval(() => setDots((e) => (e % 3) + 1), 1000)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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">
|
|
||||||
Game-PIN: <span className="underline">3169</span>
|
|
||||||
</h1>
|
|
||||||
<Icon src="gear.png" onClick={() => setSettings(true)}>
|
|
||||||
Settings
|
|
||||||
</Icon>
|
|
||||||
</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>
|
|
||||||
{settings ? <Settings closeSettings={() => setSettings(false)} /> : <></>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LobbyFrame
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
|
|
||||||
import classNames from "classnames"
|
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Player
|
|
|
@ -1,49 +0,0 @@
|
||||||
import {
|
|
||||||
faToggleLargeOff,
|
|
||||||
faToggleLargeOn,
|
|
||||||
} from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import classNames from "classnames"
|
|
||||||
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Setting
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import { useState } from "react"
|
|
||||||
import Setting from "./Setting"
|
|
||||||
|
|
||||||
const settingOptionsInit: { [key: string]: boolean } = {
|
|
||||||
allowSpectators: false,
|
|
||||||
allowSpecials: false,
|
|
||||||
allowChat: false,
|
|
||||||
allowMarkDraw: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
function Settings({ closeSettings }: { closeSettings: () => void }) {
|
|
||||||
const [settingOptions, setSettingOptions] = useState(settingOptionsInit)
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Settings
|
|
|
@ -1,17 +0,0 @@
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
function Logo() {
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Logo
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {
|
|
||||||
FontAwesomeIcon,
|
|
||||||
FontAwesomeIconProps,
|
|
||||||
} from "@fortawesome/react-fontawesome"
|
|
||||||
import { ReactNode } from "react"
|
|
||||||
|
|
||||||
function OptionButton({
|
|
||||||
icon,
|
|
||||||
action,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
icon: FontAwesomeIconProps["icon"]
|
|
||||||
action?: () => void
|
|
||||||
children: ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="flex w-full flex-row items-center justify-between rounded-lg border-2 border-black bg-voidDark py-4 pr-8 pl-16 text-4xl text-grayish first:mt-8 last:mt-8"
|
|
||||||
onClick={action}
|
|
||||||
>
|
|
||||||
<span className="mx-auto">{children}</span>
|
|
||||||
<FontAwesomeIcon className="ml-12 w-10 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
|
|
11
leaky-ships/drizzle.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import "dotenv/config"
|
||||||
|
import type { Config } from "drizzle-kit"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
schema: "./src/drizzle/schemas/Tables.ts",
|
||||||
|
out: "./src/drizzle/migrations",
|
||||||
|
driver: "pg",
|
||||||
|
dbCredentials: {
|
||||||
|
connectionString: process.env.DATABASE_URL ?? "",
|
||||||
|
},
|
||||||
|
} satisfies Config
|
83
leaky-ships/e2e/auth.spec.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { expect, test, type BrowserContext, type Page } from "@playwright/test"
|
||||||
|
|
||||||
|
let context: BrowserContext
|
||||||
|
let page: Page
|
||||||
|
|
||||||
|
test.describe.serial("Check Azure AD auth", () => {
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
context = await browser.newContext()
|
||||||
|
page = await context.newPage()
|
||||||
|
page.route(/Regelwerk\.mp4/, (route) => route.abort())
|
||||||
|
})
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await context.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Login process...", async () => {
|
||||||
|
await page.goto("/signin")
|
||||||
|
|
||||||
|
await page.locator("button#microsoft").click()
|
||||||
|
|
||||||
|
// Indicates email can be filled in
|
||||||
|
await page.waitForSelector("a#cantAccessAccount")
|
||||||
|
// Fill email input
|
||||||
|
await page.locator("input#i0116").fill(process.env.AUTH_EMAIL!)
|
||||||
|
|
||||||
|
// Click the "Next" button
|
||||||
|
await page.locator("input#idSIButton9").click()
|
||||||
|
|
||||||
|
// Indicates password can be filled in
|
||||||
|
await page.waitForSelector("a#idA_PWD_ForgotPassword")
|
||||||
|
// Fill password input
|
||||||
|
await page.locator("input#i0118").fill(process.env.AUTH_PW!)
|
||||||
|
|
||||||
|
// Click the "Sign in" button
|
||||||
|
await page.locator("input#idSIButton9").click()
|
||||||
|
|
||||||
|
// Click the "No" button
|
||||||
|
await page.locator("input#idBtn_Back").click()
|
||||||
|
|
||||||
|
await page.waitForSelector("#start")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Is logged in", async () => {
|
||||||
|
await page.goto("/signin")
|
||||||
|
await page.waitForSelector("button#signout")
|
||||||
|
await page.goto("/")
|
||||||
|
await page.waitForSelector("#start")
|
||||||
|
await page.evaluate(() => document.fonts.ready)
|
||||||
|
|
||||||
|
expect(await page.screenshot()).toMatchSnapshot("1.png", {
|
||||||
|
maxDiffPixelRatio: 0.02,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Is logged out", async () => {
|
||||||
|
await page.goto("/signout")
|
||||||
|
|
||||||
|
await page.locator("button#signout").click()
|
||||||
|
|
||||||
|
await page.waitForSelector("#start")
|
||||||
|
|
||||||
|
await page.goto(
|
||||||
|
"https://login.microsoftonline.com/common/oauth2/v2.0/logout",
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.waitForSelector("div#loginHeader")
|
||||||
|
|
||||||
|
const emailLocator = page.locator(
|
||||||
|
`[data-test-id="${process.env.AUTH_EMAIL ?? ""}"]`,
|
||||||
|
)
|
||||||
|
if (await emailLocator.isVisible()) {
|
||||||
|
await emailLocator.click()
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"The email locator is not present on the page. Skipping this step.",
|
||||||
|
)
|
||||||
|
// Optionally, you can throw an error, fail the test, or take any other desired action here.
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForSelector("div#SignOutStatusMessage")
|
||||||
|
})
|
||||||
|
})
|
BIN
leaky-ships/e2e/auth.spec.ts-snapshots/1-chromium-linux.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
leaky-ships/e2e/auth.spec.ts-snapshots/1-firefox-linux.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
leaky-ships/e2e/auth.spec.ts-snapshots/1-webkit-linux.png
Normal file
After Width: | Height: | Size: 52 KiB |
76
leaky-ships/e2e/email.spec.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
test,
|
||||||
|
type Browser,
|
||||||
|
type BrowserContext,
|
||||||
|
type Page,
|
||||||
|
} from "@playwright/test"
|
||||||
|
import { createHash, randomBytes } from "crypto"
|
||||||
|
import { and, desc, eq } from "drizzle-orm"
|
||||||
|
import db from "~/drizzle"
|
||||||
|
import { verificationTokens } from "~/drizzle/schemas/Tables"
|
||||||
|
|
||||||
|
const player1Email = (browser: Browser) =>
|
||||||
|
browser.browserType().name() + "-player-1@example.com"
|
||||||
|
|
||||||
|
let context: BrowserContext
|
||||||
|
let page: Page
|
||||||
|
|
||||||
|
test.describe.serial("Check Email auth", () => {
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
context = await browser.newContext()
|
||||||
|
page = await context.newPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await context.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Email login process...", async ({ browser }) => {
|
||||||
|
await page.goto("/signin")
|
||||||
|
|
||||||
|
await page.locator("input#email").fill(player1Email(browser))
|
||||||
|
await page.locator("button#email-submit").click()
|
||||||
|
|
||||||
|
await page.waitForURL("/api/auth/verify-request?provider=email&type=email")
|
||||||
|
|
||||||
|
const token = randomBytes(32).toString("hex")
|
||||||
|
|
||||||
|
const hash = createHash("sha256")
|
||||||
|
// Prefer provider specific secret, but use default secret if none specified
|
||||||
|
.update(`${token}${process.env.AUTH_SECRET}`)
|
||||||
|
.digest("hex")
|
||||||
|
|
||||||
|
// Use drizzle to fetch the latest token for the email
|
||||||
|
const latestToken = await db.query.verificationTokens.findFirst({
|
||||||
|
where: eq(verificationTokens.identifier, player1Email(browser)),
|
||||||
|
orderBy: [desc(verificationTokens.expires)],
|
||||||
|
})
|
||||||
|
await db
|
||||||
|
.update(verificationTokens)
|
||||||
|
.set({ token: hash })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(verificationTokens.identifier, player1Email(browser)),
|
||||||
|
eq(verificationTokens.token, latestToken?.token ?? ""),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
callbackUrl: process.env.AUTH_URL!,
|
||||||
|
token,
|
||||||
|
email: player1Email(browser),
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto("/api/auth/callback/email?" + params)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Verify Logged in...", async () => {
|
||||||
|
await page.goto("/signin")
|
||||||
|
await page.waitForSelector("button#signout")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Logging out...", async () => {
|
||||||
|
await page.locator("button#signout").click()
|
||||||
|
await page.waitForSelector("#start")
|
||||||
|
})
|
||||||
|
})
|
3
leaky-ships/global.d.ts
vendored
|
@ -1,3 +0,0 @@
|
||||||
declare module globalThis {
|
|
||||||
var prismaClient: PrismaClient
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import type { Server as HTTPServer } from "http"
|
|
||||||
import type { NextApiResponse } from "next"
|
|
||||||
import type { Socket as NetSocket } from "net"
|
|
||||||
import type { Server as IOServer } from "socket.io"
|
|
||||||
|
|
||||||
interface SocketServer extends HTTPServer {
|
|
||||||
io?: IOServer | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SocketWithIO extends NetSocket {
|
|
||||||
server: SocketServer
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NextApiResponseWithSocket extends NextApiResponse {
|
|
||||||
socket: SocketWithIO
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
export interface Position {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
export interface Target extends Position {
|
|
||||||
preview: boolean
|
|
||||||
show: boolean
|
|
||||||
}
|
|
||||||
export interface MouseCursor extends Position {
|
|
||||||
shouldShow: boolean
|
|
||||||
}
|
|
||||||
export interface TargetList extends Position {
|
|
||||||
type: string
|
|
||||||
edges: string[]
|
|
||||||
}
|
|
||||||
export interface Mode {
|
|
||||||
pointerGrid: any[][]
|
|
||||||
type: string
|
|
||||||
}
|
|
||||||
export interface Items {
|
|
||||||
icon: string
|
|
||||||
text: string
|
|
||||||
mode?: number
|
|
||||||
amount?: number
|
|
||||||
}
|
|
||||||
export interface Field extends Position {
|
|
||||||
field: string
|
|
||||||
}
|
|
||||||
export interface Hit extends Position {
|
|
||||||
hit: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface fireMissile {
|
|
||||||
type: "fireMissile"
|
|
||||||
payload: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
hit: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interface removeMissile {
|
|
||||||
type: "removeMissile"
|
|
||||||
payload: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
hit: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HitDispatch = fireMissile | removeMissile
|
|
5
leaky-ships/kill-server.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
server_pid=$(lsof -i :3000 -t)
|
||||||
|
if [[ -n $server_pid ]]; then
|
||||||
|
echo "Killing server..." $server_pid
|
||||||
|
kill -9 $server_pid
|
||||||
|
fi
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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,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)
|
|
||||||
}
|
|
|
@ -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/Gamefield/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
|
|
|
@ -1,15 +0,0 @@
|
||||||
// lib/prisma.ts
|
|
||||||
import { PrismaClient } from "@prisma/client"
|
|
||||||
|
|
||||||
let prisma: PrismaClient
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
|
||||||
prisma = new PrismaClient()
|
|
||||||
} else {
|
|
||||||
if (!global.prismaClient) {
|
|
||||||
global.prismaClient = new PrismaClient()
|
|
||||||
}
|
|
||||||
prisma = global.prismaClient
|
|
||||||
}
|
|
||||||
|
|
||||||
export default prisma
|
|
|
@ -1,51 +0,0 @@
|
||||||
import { Hit, HitDispatch } from "../../interfaces/frontend"
|
|
||||||
|
|
||||||
export function borderCN(count: number, x: number, y: number) {
|
|
||||||
if (x === 0) return "left"
|
|
||||||
if (y === 0) return "top"
|
|
||||||
if (x === count + 1) return "right"
|
|
||||||
if (y === count + 1) return "bottom"
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
export function cornerCN(count: number, x: number, y: number) {
|
|
||||||
if (x === 0 && y === 0) return "left-top-corner"
|
|
||||||
if (x === count + 1 && y === 0) return "right-top-corner"
|
|
||||||
if (x === 0 && y === count + 1) return "left-bottom-corner"
|
|
||||||
if (x === count + 1 && y === count + 1) return "right-bottom-corner"
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
export function fieldIndex(count: number, x: number, y: number) {
|
|
||||||
return y * (count + 2) + x
|
|
||||||
}
|
|
||||||
export function hitReducer(formObject: Hit[], action: HitDispatch) {
|
|
||||||
switch (action.type) {
|
|
||||||
case "fireMissile": {
|
|
||||||
const result = [...formObject, action.payload]
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return formObject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const initlialLastLeftTile = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
}
|
|
||||||
export const initlialTarget = {
|
|
||||||
preview: false,
|
|
||||||
show: false,
|
|
||||||
x: 2,
|
|
||||||
y: 2,
|
|
||||||
}
|
|
||||||
export const initlialTargetPreview = {
|
|
||||||
preview: true,
|
|
||||||
show: false,
|
|
||||||
x: 2,
|
|
||||||
y: 2,
|
|
||||||
}
|
|
||||||
export const initlialMouseCursor = {
|
|
||||||
shouldShow: false,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
|
7114
leaky-ships/package-lock.json
generated
|
@ -1,54 +1,80 @@
|
||||||
{
|
{
|
||||||
"name": "leaky-ships",
|
"name": "leaky-ships",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "vinxi dev",
|
||||||
"build": "next build",
|
"build": "vinxi build",
|
||||||
"start": "next start",
|
"start": "vinxi start",
|
||||||
"lint": "next lint"
|
"lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"",
|
||||||
|
"push": "drizzle-kit push:pg",
|
||||||
|
"test": "pnpm playwright test --ui",
|
||||||
|
"typecheck": "tsc --noEmit --checkJs false --skipLibCheck"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@auth/core": "^0.27.0",
|
||||||
"@fortawesome/pro-duotone-svg-icons": "^6.2.1",
|
"@auth/drizzle-adapter": "^0.7.0",
|
||||||
"@fortawesome/pro-light-svg-icons": "^6.2.1",
|
"@auth/solid-start": "^0.6.1",
|
||||||
"@fortawesome/pro-regular-svg-icons": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
"@fortawesome/pro-solid-svg-icons": "^6.2.1",
|
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/pro-thin-svg-icons": "^6.2.1",
|
"@fortawesome/pro-light-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/sharp-solid-svg-icons": "^6.2.1",
|
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||||
"@next/font": "13.1.1",
|
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
|
||||||
"@prisma/client": "^4.9.0",
|
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
|
||||||
"bcrypt": "^5.1.0",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"classnames": "^2.3.2",
|
"@solidjs/meta": "^0.29.3",
|
||||||
|
"@solidjs/router": "^0.12.4",
|
||||||
|
"@solidjs/start": "^0.5.9",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"cookies-next": "^2.1.1",
|
"drizzle-orm": "^0.29.4",
|
||||||
"eslint": "8.31.0",
|
"drizzle-zod": "^0.5.1",
|
||||||
"eslint-config-next": "13.1.1",
|
"http-status": "^1.7.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"json-stable-stringify": "^1.1.1",
|
||||||
"next": "13.1.1",
|
"lodash-es": "^4.17.21",
|
||||||
"prisma": "^4.9.0",
|
"nodemailer": "^6.9.10",
|
||||||
"react": "18.2.0",
|
"object-hash": "^3.0.0",
|
||||||
"react-dom": "18.2.0",
|
"postgres": "^3.4.3",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.7.4",
|
||||||
"socket.io-client": "^4.5.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"typescript": "4.9.4",
|
"solid-color": "^0.0.4",
|
||||||
"uuid": "^9.0.0"
|
"solid-js": "^1.8.15",
|
||||||
|
"tinycolor2": "^1.6.0",
|
||||||
|
"unique-names-generator": "^4.7.1",
|
||||||
|
"vinxi": "^0.3.3",
|
||||||
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
|
"packageManager": "pnpm@8.7.4",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@playwright/test": "^1.41.2",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/node": "^18.11.18",
|
"@types/json-stable-stringify": "^1.0.36",
|
||||||
"@types/react": "^18.0.27",
|
"@types/node": "^20.11.19",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/object-hash": "^3.0.6",
|
||||||
"@types/web-bluetooth": "^0.0.16",
|
"@types/web-bluetooth": "^0.0.20",
|
||||||
"autoprefixer": "^10.4.13",
|
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.4.21",
|
"dotenv": "^16.4.5",
|
||||||
"prettier": "^2.8.3",
|
"drizzle-kit": "^0.20.14",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"eslint": "^8.56.0",
|
||||||
"sass": "^1.57.1",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"tailwindcss": "^3.2.4"
|
"eslint-plugin-solid": "^0.13.1",
|
||||||
|
"pg": "^8.11.3",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
|
"sass": "^1.71.1",
|
||||||
|
"solid-start-node": "^0.3.10",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.1.4"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"@auth/core": "^0.13.0",
|
||||||
|
"solid-start": "^0.3.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import "../styles/App.scss"
|
|
||||||
import "../styles/grid.scss"
|
|
||||||
import "../styles/grid2.scss"
|
|
||||||
import "../styles/homepage.scss"
|
|
||||||
import "../styles/globals.css"
|
|
||||||
import type { AppProps } from "next/app"
|
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
|
||||||
return <Component {...pageProps} />
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { Html, Head, Main, NextScript } from "next/document"
|
|
||||||
|
|
||||||
export default function Document() {
|
|
||||||
return (
|
|
||||||
<Html lang="en">
|
|
||||||
<Head />
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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[],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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[],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,32 +0,0 @@
|
||||||
import type { NextApiRequest } from "next"
|
|
||||||
import { Server } from "socket.io"
|
|
||||||
import { NextApiResponseWithSocket } from "../../interfaces/NextApiSocket"
|
|
||||||
|
|
||||||
const SocketHandler = (req: NextApiRequest, res: NextApiResponseWithSocket) => {
|
|
||||||
if (res.socket.server.io) {
|
|
||||||
console.log("Socket is already running " + req.url)
|
|
||||||
} else {
|
|
||||||
console.log("Socket is initializing " + req.url)
|
|
||||||
const io = new Server(res.socket.server)
|
|
||||||
res.socket.server.io = io
|
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
|
||||||
socket.on("input-change", (msg) => {
|
|
||||||
socket.broadcast.emit("update-input", msg)
|
|
||||||
})
|
|
||||||
// console.log(socket.id)
|
|
||||||
// console.log(socket)
|
|
||||||
// ...
|
|
||||||
|
|
||||||
socket.on("test", (payload) => {
|
|
||||||
console.log("Got test:", payload)
|
|
||||||
// ...
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.emit("test2", "lol")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
res.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SocketHandler
|
|
|
@ -1,22 +0,0 @@
|
||||||
import Head from "next/head"
|
|
||||||
import Gamefield from "../../components/Gamefield/Gamefield"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Create Next App</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<Gamefield />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import Head from "next/head"
|
|
||||||
import Grid from "../../components/Grid"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Create Next App</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<Grid />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import Head from "next/head"
|
|
||||||
import Grid2 from "../../components/Grid2"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Create Next App</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<Grid2 />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,47 +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"
|
|
||||||
import Logo from "../../components/Logo"
|
|
||||||
import BurgerMenu from "../../components/BurgerMenu"
|
|
||||||
import OptionButton from "../../components/OptionButton"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [heWantsToPlay, setHeWantsToPlay] = useState<boolean | null>(false)
|
|
||||||
return (
|
|
||||||
<div className="h-full bg-theme">
|
|
||||||
<BurgerMenu />
|
|
||||||
<div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-around">
|
|
||||||
<Logo />
|
|
||||||
{!heWantsToPlay ? (
|
|
||||||
<>
|
|
||||||
<div className="transition-color flex h-full max-h-80 w-full max-w-screen-sm items-center justify-center rounded-xl border-4 border-black bg-[#2227] duration-200">
|
|
||||||
<FontAwesomeIcon className="text-8xl" icon={faCirclePlay} />
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="font-farro transition-color rounded-2xl border-b-8 border-orange-400 bg-warn px-24 pt-10 pb-8 text-5xl font-bold duration-100 active:border-t-8 active:border-b-0"
|
|
||||||
onClick={() => setTimeout(() => setHeWantsToPlay(true), 200)}
|
|
||||||
>
|
|
||||||
START
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="gap flex w-full flex-col items-center rounded-xl border-4 border-black bg-grayish p-12 shadow-lg">
|
|
||||||
<button
|
|
||||||
className="mt-[-1.5rem] w-40 self-start rounded-lg border-2 border-black bg-voidDark px-2 text-5xl text-grayish"
|
|
||||||
onClick={() => setHeWantsToPlay(false)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faLeftLong} />
|
|
||||||
</button>
|
|
||||||
<div className="flex flex-col items-center gap-12">
|
|
||||||
<OptionButton icon={faPlus}>Raum erstellen</OptionButton>
|
|
||||||
<OptionButton icon={faUserPlus}>Raum beitreten</OptionButton>
|
|
||||||
<OptionButton icon={faEye}>Zuschauen</OptionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import Head from "next/head"
|
|
||||||
import Logo from "../../components/Logo"
|
|
||||||
import LobbyFrame from "../../components/Lobby/LobbyFrame"
|
|
||||||
import BurgerMenu from "../../components/BurgerMenu"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
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>
|
|
||||||
<BurgerMenu />
|
|
||||||
<Logo />
|
|
||||||
<LobbyFrame />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
||||||
import React, { useState } from "react"
|
|
||||||
|
|
||||||
function login() {
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
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">
|
|
||||||
{loading ? "Logging in..." : "Enter Login Details"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{loading ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="my-16 mx-24 animate-spin text-6xl"
|
|
||||||
icon={faSpinnerThird}
|
|
||||||
onClick={() => setLoading(false)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<form action="#">
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</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"
|
|
||||||
/>
|
|
||||||
</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"
|
|
||||||
onClick={() => setLoading(true)}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default login
|
|
|
@ -1,38 +0,0 @@
|
||||||
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)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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="my-16 mx-24 animate-spin text-6xl"
|
|
||||||
icon={faSpinnerThird}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default logout
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import Head from "next/head"
|
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Create Next App</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/gamefield" target="_blank">
|
|
||||||
Gamefield
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev" target="_blank">
|
|
||||||
Homepage
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/Lobby" target="_blank">
|
|
||||||
Lobby
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/login" target="_blank">
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/logout" target="_blank">
|
|
||||||
Logout
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link href="/dev/grid" target="_blank">
|
|
||||||
Grid Effect
|
|
||||||
</Link>
|
|
||||||
</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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
78
leaky-ships/playwright.config.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { defineConfig, devices } from "@playwright/test"
|
||||||
|
import dotenv from "dotenv"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./e2e",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: process.env.AUTH_URL!,
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: "pnpm start",
|
||||||
|
url: process.env.AUTH_URL,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
})
|
8606
leaky-ships/pnpm-lock.yaml
generated
7
leaky-ships/prettier.config.cjs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
semi: false,
|
||||||
|
plugins: [
|
||||||
|
"prettier-plugin-organize-imports",
|
||||||
|
"prettier-plugin-tailwindcss", // MUST come last
|
||||||
|
],
|
||||||
|
}
|
|
@ -1,78 +1,168 @@
|
||||||
|
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 = "postgres"
|
||||||
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?
|
||||||
email String @unique @default("")
|
access_token String?
|
||||||
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?
|
||||||
|
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?
|
||||||
token String @unique
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
type TokenType
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
used Boolean @default(false)
|
games User_Game[]
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String @unique
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@unique([identifier, token])
|
||||||
|
@@map("verificationtokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GameState {
|
||||||
|
lobby
|
||||||
|
starting
|
||||||
|
running
|
||||||
|
ended
|
||||||
|
aborted
|
||||||
}
|
}
|
||||||
|
|
||||||
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(lobby)
|
||||||
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
|
}
|
||||||
|
|
||||||
|
enum Orientation {
|
||||||
|
h
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
model Ship {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
size Int
|
||||||
|
variant Int
|
||||||
|
x Int
|
||||||
|
y Int
|
||||||
|
orientation Orientation
|
||||||
|
user_GameId String
|
||||||
|
User_Game User_Game @relation(fields: [user_GameId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Hit {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
x Int
|
||||||
|
y Int
|
||||||
|
hit Boolean
|
||||||
|
user_GameId String
|
||||||
|
User_Game User_Game @relation(fields: [user_GameId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model User_Game {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
gameId String
|
||||||
|
userId String
|
||||||
|
index Int
|
||||||
|
moves Move[]
|
||||||
|
ships Ship[]
|
||||||
|
hits Hit[]
|
||||||
|
chats Chat[]
|
||||||
|
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@unique([gameId, index])
|
||||||
|
@@unique([gameId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MoveType {
|
||||||
|
missile
|
||||||
|
vtorpedo
|
||||||
|
htorpedo
|
||||||
|
radar
|
||||||
|
}
|
||||||
|
|
||||||
|
model Move {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
index Int
|
||||||
|
type MoveType
|
||||||
|
x Int
|
||||||
|
y Int
|
||||||
|
orientation Orientation
|
||||||
|
user_game_id String
|
||||||
|
user_game User_Game @relation(fields: [user_game_id], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
BIN
leaky-ships/public/Regelwerk.mp4
Normal file
BIN
leaky-ships/public/assets/player_england.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
leaky-ships/public/assets/player_oktay.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
leaky-ships/public/assets/player_vietnam.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
BIN
leaky-ships/public/assets/ship_england_2_1.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
leaky-ships/public/assets/ship_england_3_1.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
leaky-ships/public/assets/ship_england_4_1.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
leaky-ships/public/assets/ship_oktay_2_1.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
leaky-ships/public/assets/ship_oktay_3_1.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
leaky-ships/public/assets/ship_oktay_4_1.png
Normal file
After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
leaky-ships/public/assets/ship_vietnam_2_1.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
leaky-ships/public/assets/ship_vietnam_3_1.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
leaky-ships/public/assets/ship_vietnam_4_1.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
1
leaky-ships/public/images/Microsoft_icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><path fill="#f35325" d="M0 0h10v10H0z"/><path fill="#81bc06" d="M11 0h10v10H11z"/><path fill="#05a6f0" d="M0 11h10v10H0z"/><path fill="#ffba08" d="M11 11h10v10H11z"/></svg>
|
After Width: | Height: | Size: 232 B |
BIN
leaky-ships/public/images/logo-gbs.png
Normal file
After Width: | Height: | Size: 109 KiB |
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|