Merge branch 'front' into docker-front

This commit is contained in:
Adam
2025-10-16 13:32:12 +02:00
committed by GitHub
30 changed files with 2270 additions and 25 deletions

View File

@ -11,7 +11,6 @@ GRAPH_PORT=3000
ELK_PORT=5601
GOOGLE_CALLBACK_URL=https://localhost:8443/api/v1
# the url to which the user will be redirected when it logs with google
CALLBACK_REDIR=http://localhost:3000
GOOGLE_CLIENT_SECRET=susAF
@ -29,3 +28,4 @@ EMAIL_TO=<mail to send to>
USER_URL=<the url to the user api>
AUTH_URL=<the url to the auth api>
CORS_ORIGIN=<the url of origin for cors>

View File

@ -15,6 +15,7 @@ services:
- API_TARGET=user
- LOG_FILE_PATH=/var/log/log.log
- JWT_SECRET=${JWT_SECRET}
- CORS_ORIGIN=${CORS_ORIGIN}
restart: unless-stopped
auth-api:
container_name: transcendence-api-auth
@ -35,6 +36,7 @@ services:
- API_TARGET=auth
- LOG_FILE_PATH=/var/log/log.log
- JWT_SECRET=${JWT_SECRET}
- CORS_ORIGIN=${CORS_ORIGIN}
restart: unless-stopped
scorestore-api:
container_name: transcendence-api-scoreStore

6
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1753250450,
"narHash": "sha256-i+CQV2rPmP8wHxj0aq4siYyohHwVlsh40kV89f3nw1s=",
"lastModified": 1756542300,
"narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "fc02ee70efb805d3b2865908a13ddd4474557ecf",
"rev": "d7600c775f877cd87b4f5a831c28aa94137377aa",
"type": "github"
},
"original": {

View File

@ -39,7 +39,7 @@
nodejs_22
pnpm
just
foundry
foundry
];
shellHook = ''
if [ ! -d node_modules/ ]; then
@ -50,6 +50,7 @@
echo Installing foundry env
forge i
fi
alias jarvis=just
export PATH+=:$(pwd)/node_modules/.bin
echo entering ft_trans env
'';

View File

@ -2,6 +2,7 @@
"dependencies": {
"@avalabs/avalanchejs": "^5.0.0",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
"@fastify/env": "^5.0.2",
"@fastify/jwt": "^9.1.0",
"axios": "^1.10.0",

11
pnpm-lock.yaml generated
View File

@ -14,6 +14,9 @@ importers:
'@fastify/cookie':
specifier: ^11.0.2
version: 11.0.2
'@fastify/cors':
specifier: ^11.1.0
version: 11.1.0
'@fastify/env':
specifier: ^5.0.2
version: 5.0.2
@ -255,6 +258,9 @@ packages:
'@fastify/cookie@11.0.2':
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
'@fastify/cors@11.1.0':
resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==}
'@fastify/deepmerge@2.0.2':
resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==}
@ -1758,6 +1764,11 @@ snapshots:
cookie: 1.0.2
fastify-plugin: 5.0.1
'@fastify/cors@11.1.0':
dependencies:
fastify-plugin: 5.0.1
toad-cache: 3.7.0
'@fastify/deepmerge@2.0.2': {}
'@fastify/env@5.0.2':

View File

@ -1,5 +1,6 @@
import fastifyJWT from '@fastify/jwt';
import fastifyCookie from '@fastify/cookie';
import cors from '@fastify/cors'
import { register } from './register.js';
import { login } from './login.js';
@ -23,6 +24,12 @@ authDB.prepareDB();
*/
export default async function(fastify, options) {
fastify.register(cors, {
origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
methods: [ "GET", "POST", "DELETE", "OPTIONS" ]
});
fastify.register(fastifyJWT, {
secret: process.env.JWT_SECRET || '123456789101112131415161718192021',
cookie: {

View File

@ -1,5 +1,6 @@
import fastifyJWT from '@fastify/jwt';
import fastifyCookie from '@fastify/cookie';
import cors from '@fastify/cors'
import Database from 'better-sqlite3';
var env = process.env.NODE_ENV || 'development';
@ -56,6 +57,13 @@ const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;'
* @param {import('fastify').FastifyPluginOptions} options
*/
export default async function(fastify, options) {
fastify.register(cors, {
origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
methods: [ "GET", "POST", "DELETE", "OPTIONS" ]
});
fastify.register(fastifyJWT, {
secret: process.env.JWT_SECRET || '123456789101112131415161718192021',
cookie: {

View File

@ -1,19 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Tailwind Test</title>
<link href="/style.css" rel="stylesheet" />
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
<div class="text-center p-10 bg-white rounded-xl shadow space-y-4">
<h1 class="text-4xl font-bold text-blue-600">Vite + Tailwind</h1>
<p class="text-gray-700 text-lg">🚀 Looks like it's working!</p>
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Click Me
</button>
</div>
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/static/assets/favicon.ico" rel="icon" type="image/x-icon" >
<title>Vite + Tailwind Test</title>
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
</head>
<!--body class="bg-gray-100 dark:bg-neutral-950 h-screen flex flex-col"-->
<body class="bg-[url(https://y-syo.me/res/bg.jpg)] dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col">
<!--script src="https://kanel.ovh/oneko.js"></script-->
<!--script src="./static/ts/oneko.js"></script-->
<div class="absolute flex flex-col items-center space-y-5 top-4 left-5">
<!--a class="absolute flex flex-col items-center top-4 left-5" href="/pong" data-icon-->
<a class="flex flex-col items-center" href="/pong" data-icon>
<img src="./static/assets/pong.svg" width=32 height=32 />
<span class="text-white font-[Kubasta]">pong_game.ts</span>
</a>
<a class="flex flex-col items-center" href="/tetris" data-icon>
<img src="./static/assets/tetrio.svg" width=32 height=32 />
<span class="text-white font-[Kubasta]">tetris_game.ts</span>
</a>
<a class="flex flex-col items-center" href="https://tetr.io/">
<img src="./static/assets/tetrio.svg" width=32 height=32 />
<span class="text-white font-[Kubasta]">tetr.io</span>
</a>
</div>
<div id="app" class="flex-1 flex items-center justify-center">
</div>
<div id="taskbar-menu" class="absolute bottom-13 left-0"></div>
<div class="border-t-2 border-neutral-300 dark:border-neutral-800 sticky bottom-0">
<nav class="bg-neutral-200 dark:bg-neutral-900 shadow-md border-t-2 border-neutral-400 dark:border-neutral-700 flex justify-between h-12 items-center content-center space-x-6 font-[Kubasta]">
<div class="flex px-4 items-center content-center space-x-2">
<button id="profile-button" class="taskbar-button flex flex-row justify-center items-center"><img class="object-scale-down mr-2 h-5 w-5" src="https://api.kanel.ovh/id?id=65" /> start</button>
<div class="text-neutral-700 dark:text-neutral-400">|</div>
<a class="taskbar-button" href="https://rusty.42angouleme.fr/">rusty</a>
<a class="taskbar-button" href="/tetris" data-link>tetris</a>
</div>
<div class="reverse-border m-1.5 h-8/10 content-center">
<span id="taskbar-clock" class="text- neutral-900 dark:text-white px-4">12:37</span>
</div>
</nav>
</div>
<script type="module" src="/static/ts/main.ts"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,17 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1080" width="1080" height="1080">
<defs>
<image width="1080" height="1080" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4AQMAAADW3v7MAAAAAXNSR0IB2cksfwAAAANQTFRFAAAAp3o92gAAA0hJREFUeJztzjEBACAMwDDm3zTPniiAo1GQOX+Y14HVQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQxfkswQ5pU9WdQAAAABJRU5ErkJggg=="/>
<image width="97" height="97" id="img2" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAQMAAAD8yF3gAAAAAXNSR0IB2cksfwAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/v/Rru6GAAAAHUlEQVR4nGOsb2CAg0bGUd4ob5Q3yhvlEctjQAYA62mQYokW47wAAAAASUVORK5CYII="/>
<image width="97" height="97" id="img3" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAQMAAAD8yF3gAAAAAXNSR0IB2cksfwAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/wDltzBKAAAAHUlEQVR4nGNkQAaMo7xR3ihvpPL+M/xnGuVRwAMA3qIuYqMkUaQAAAAASUVORK5CYII="/>
<image width="97" height="97" id="img4" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAgMAAAC7aCcwAAAAAXNSR0IB2cksfwAAAAlQTFRF////////////jkrl/gAAAAN0Uk5T/v8A1ghndgAAACtJREFUeJxjFHVgwApeM47KjMqMyozKjMqMyozK0FJmFYj6v2ZUZlSGejIAqSynoiv80kgAAAAASUVORK5CYII="/>
</defs>
<style>
.s0 { fill: #ffffff }
</style>
<use id="Background" href="#img1" x="0" y="0"/>
<path id="Layer 1" fill-rule="evenodd" class="s0" d="m1008 746v97h-937v-97z"/>
<path id="Shape 1" fill-rule="evenodd" class="s0" d="m729 459v97h-97v-97z"/>
<use id="Shape 1 copy" style="opacity: .7" href="#img2" x="706" y="385"/>
<use id="Shape 1 copy 2" style="opacity: .4" href="#img3" x="780" y="311"/>
<use id="Shape 1 copy 3" style="opacity: .2" href="#img4" x="854" y="237"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="60mm"
height="60mm"
viewBox="0 0 60 60"
version="1.1"
id="svg9638"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="tetrio.svg"
inkscape:export-filename="D:\Projects\tetrio\client\res\tetriox256.png"
inkscape:export-xdpi="108.37334"
inkscape:export-ydpi="108.37334">
<defs
id="defs9632" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="121.84373"
inkscape:cy="101.26992"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1676"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata9635">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-237)">
<rect
style="opacity:0.07000002;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.29149818;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14257"
width="55"
height="55"
x="2.05"
y="239.5" />
<rect
style="opacity:0.07000002;fill:#000000;fill-opacity:1;stroke:none;stroke-width:20.26499748;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14257-7"
width="50"
height="50"
x="5"
y="242" />
<rect
style="opacity:0.07000002;fill:#0e0b0e;fill-opacity:1;stroke:none;stroke-width:18.23849869;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14257-7-6"
width="45"
height="45"
x="7.5"
y="244.5" />
<path
style="opacity:1;fill:#df4eaa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 0,237 v 10 h 10 v 10 h 10 v -10 h 10 v -10 z"
id="path14118"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<rect
style="opacity:1;fill:#c040aa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14120"
width="10"
height="40"
x="10"
y="257" />
<rect
style="opacity:1;fill:#7e5fe3;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14120-0"
width="10"
height="40"
x="20"
y="257" />
<path
style="opacity:1;fill:#2f51aa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 30,287 H 50 V 277 H 40 V 257 H 30 Z"
id="path14118-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#15919d;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 60,247 H 40 v 10 h 10 v 20 h 10 z"
id="path14118-8-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,78 @@
@import "tailwindcss";
@font-face {
font-family: Kubasta;
src: url('../assets/fonts/Kubasta.otf') format("opentype");
}
@source inline("space-y-{18,46,102,214,438,886,1782,3574,7158,14326,28662,57334,114678,229366,458742,917494}");
@source inline("mt-{28,56,84,112}");
@theme {
--shadow-2x1: 2px 2px 0px black;
/*
--color-kanel-700: #ac5c24;
*/
}
.default-border {
@apply border-2
border-t-neutral-100 border-l-neutral-100 border-r-neutral-400 border-b-neutral-400
dark:border-t-neutral-500 dark:border-l-neutral-500 dark:border-r-neutral-700 dark:border-b-neutral-700
;
}
.reverse-border {
@apply border-2
border-t-neutral-400 border-l-neutral-400 border-r-neutral-100 border-b-neutral-100
dark:border-t-neutral-700 dark:border-l-neutral-700 dark:border-r-neutral-500 dark:border-b-neutral-500
;
}
.input-border {
@apply border-2
border-t-neutral-950 border-l-neutral-950 border-r-neutral-200 border-b-neutral-200
dark:border-t-neutral-950 dark:border-l-neutral-950 dark:border-r-neutral-600 dark:border-b-neutral-600
;
}
.default-button {
@apply shadow-2x1
bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-700
text-neutral-900 dark:text-white
px-4 py-2
delay-0 duration-150 transition-colors
border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700
active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500
;
}
.taskbar-button {
@apply shadow-2x1
bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-700
text-neutral-900 dark:text-white
px-4 py-0.5
content-center text-center
delay-0 duration-150 transition-colors
border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700
active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500
;
}
.menu-default-button {
@apply w-46 h-12
text-neutral-900 dark:text-white
bg-neutral-200 hover:bg-neutral-300
dark:bg-neutral-800 dark:hover:bg-neutral-700
;
}
.menu-default-label {
@apply w-46 h-12
text-neutral-900 dark:text-white
bg-neutral-200
dark:bg-neutral-800
;
}

118
src/front/static/ts/main.ts Normal file
View File

@ -0,0 +1,118 @@
import { oneko } from "./oneko.ts";
import Profile from "./views/Profile.ts";
let profile_view = new Profile;
export async function isLogged(): Promise<boolean> {
let uuid_req = await fetch("http://localhost:3001/me", {
method: "GET",
credentials: "include",
});
if (uuid_req.status === 200)
{
let uuid = await uuid_req.json();
document.cookie = `uuid=${uuid.user};max-age=${60*60*24*7}`;
return true;
}
else // 401
{
document.cookie = `uuid=;max-age=0`;
return false;
}
}
export const navigationManager = url => {
history.pushState(null, null, url);
router();
};
let view;
const routes = [
{ path: "/", view: () => import("./views/MainMenu.ts") },
{ path: "/pong", view: () => import("./views/PongMenu.ts") },
{ path: "/pong/local", view: () => import("./views/Game.ts") },
{ path: "/pong/tournament", view: () => import("./views/TournamentMenu.ts") },
{ path: "/tetris", view: () => import("./views/TetrisMenu.ts") },
{ path: "/tetris/solo", view: () => import("./views/Tetris.ts") },
{ path: "/tetris/versus", view: () => import("./views/Tetris.ts") },
{ path: "/login", view: () => import("./views/LoginPage.ts") },
{ path: "/register", view: () => import("./views/RegisterPage.ts") },
];
const router = async () => {
const routesMap = routes.map(route => {
return { route: route, isMatch: location.pathname === route.path };
});
let match = routesMap.find(routeMap => routeMap.isMatch);
if (!match)
match = { route: routes[0], isMatch: true };
if (view)
view.running = false;
//console.log(match);
const module = await match.route.view();
view = new module.default();
document.querySelector("#app").innerHTML = await view.getHTML();
view.run();
};
document.getElementById("profile-button")?.addEventListener("click", () => {profile_view.run();});
window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => {
isLogged();
document.body.addEventListener("click", e=> {
if (!e.target.closest("#taskbar-menu") && !e.target.matches("#profile-button"))
{
profile_view.open = false;
document.getElementById("taskbar-menu").innerHTML = "";
}
if (e.target.matches("[data-link]"))
{
e.preventDefault();
navigationManager(e.target.href);
}
if (e.target.closest("[data-icon]"))
e.preventDefault();
});
document.body.addEventListener("dblclick", e=> {
if (e.target.closest("[data-icon]"))
{
e.preventDefault();
navigationManager(e.target.closest("[data-icon]").href);
}
});
router();
});
oneko();
function updateClock()
{
const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const clock = document.getElementById("taskbar-clock");
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
hours = hours < 10 ? "0" + hours : hours;
minutes = minutes < 10 ? "0" + minutes : minutes;
clock.innerHTML = `${days[now.getDay()]} ${now.getDate()} ${months[now.getMonth()]} ` + hours + ":" + minutes;
}
setInterval(updateClock, 5000);
updateClock();

View File

@ -0,0 +1,273 @@
// oneko.js: https://github.com/adryd325/oneko.js
// edited by yosyo specificely for knl_meowscendence.
let oneko_state: number = 0; // 0 = normal, 1 = pong, 2 = tetris
let mousePosX: number = 0;
let mousePosY: number = 0;
let offsetX: number = 0;
let offsetY: number = 0;
export function setOnekoState(state: string) {
switch (state) {
case "pong":
oneko_state = 1;
break;
case "tetris":
oneko_state = 2;
break;
default:
oneko_state = 0;
}
}
export function setOnekoOffset() {
if (oneko_state == 1)
{
offsetX = document.getElementById("window").offsetLeft + 44;
offsetY = document.getElementById("window").offsetTop + 44 + 24;
console.log(offsetX, offsetY);
}
}
export function setBallPos(x: number, y: number) {
mousePosX = x + offsetX;
mousePosY = y + offsetY;
}
export function oneko() {
const isReducedMotion =
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
if (isReducedMotion) return;
const nekoEl = document.createElement("div");
let nekoPosX = 256;
let nekoPosY = 256;
let frameCount = 0;
let idleTime = 0;
let idleAnimation = null;
let idleAnimationFrame = 0;
const nekoSpeed = 10;
const spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "fixed";
nekoEl.style.pointerEvents = "none";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
nekoEl.style.zIndex = 2147483647;
let nekoFile = "https://kanel.ovh/assets/oneko.gif"
const curScript = document.currentScript
if (curScript && curScript.dataset.cat) {
nekoFile = curScript.dataset.cat
}
nekoEl.style.backgroundImage = `url(${nekoFile})`;
document.body.appendChild(nekoEl);
document.addEventListener("mousemove", function (event) {
if (oneko_state == 0)
{
mousePosX = event.clientX;
mousePosY = event.clientY;
}
});
window.requestAnimationFrame(onAnimationFrame);
}
let lastFrameTimestamp;
function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM
if (!nekoEl.isConnected) {
return;
}
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp
frame()
}
window.requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) {
const sprite = spriteSets[name][frame % spriteSets[name].length];
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime += 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame += 1;
}
function frame() {
frameCount += 1;
const diffX = nekoPosX - mousePosX;
const diffY = nekoPosY - mousePosY;
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
let direction;
direction = diffY / distance > 0.5 ? "N" : "";
direction += diffY / distance < -0.5 ? "S" : "";
direction += diffX / distance > 0.5 ? "W" : "";
direction += diffX / distance < -0.5 ? "E" : "";
setSprite(direction, frameCount);
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
}
init();
}

View File

@ -0,0 +1,10 @@
export default class {
contructor()
{
}
setTitle(title) { document.title = title; }
async getHTML() { return ""; }
async run() { }
};

View File

@ -0,0 +1,289 @@
import Aview from "./Aview.ts"
import { isLogged } from "../main.js"
import { dragElement } from "./drag.js"
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts"
export default class extends Aview {
running: boolean;
constructor()
{
super();
this.setTitle("pong (local match)");
this.running = true;
setOnekoState("default");
}
async getHTML() {
return `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">pong_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 space-y-4 reverse-border">
<div id="player-inputs" class="flex flex-col space-y-4">
<div class="flex flex-row">
<span class="reverse-border w-full ml-2"><input type="text" id="player1" placeholder="Player 1" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input></span>
<span class="reverse-border w-full ml-2"><input type="text" id="player2" placeholder="Player 2" class="bg-white text-neutral-900 px-4 py-2 w-full input-border" required></input></span>
</div>
<button id="game-start" class="default-button">play</button>
</div>
<div id="game-buttons" class="hidden flex mt-4">
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
<a id="game-back" class="default-button w-full mx-4 py-2" href="/pong" data-link>back</a>
</div>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
let start: number = 0;
let elapsed: number;
let game_playing: boolean = false;
let match_over: boolean = false;
let p1_score: number = 0;
let p2_score: number = 0;
let p1_name: string;
let p2_name: string;
let countdown: number = 3;
let countdownTimer: number = 0;
let canvas;
let ctx;
const paddleOffset: number = 15;
const paddleHeight: number = 100;
const paddleWidth: number = 10;
const ballSize: number = 10;
const paddleSpeed: number = 727 * 0.69;
let leftPaddleY: number;
let rightPaddleY: number;
let ballX: number;
let ballY: number;
let ballSpeed: number = 200;
let ballSpeedX: number = 300;
let ballSpeedY: number = 10;
const keys: Record<string, boolean> = {};
document.addEventListener("keydown", e => { keys[e.key] = true; });
document.addEventListener("keyup", e => { keys[e.key] = false; });
function movePaddles() {
if ((keys["w"] || keys["W"]) && leftPaddleY > 0)
leftPaddleY -= paddleSpeed * elapsed;
if ((keys["s"] || keys["S"]) && leftPaddleY < canvas.height - paddleHeight)
leftPaddleY += paddleSpeed * elapsed;
if (keys["ArrowUp"] && rightPaddleY > 0)
rightPaddleY -= paddleSpeed * elapsed;
if (keys["ArrowDown"] && rightPaddleY < canvas.height - paddleHeight)
rightPaddleY += paddleSpeed * elapsed;
}
function getBounceVelocity(paddleY) {
const speed = ballSpeed;
const paddleCenterY = paddleY + paddleHeight / 2;
let n = (ballY - paddleCenterY) / (paddleHeight / 2);
n = Math.max(-1, Math.min(1, n));
let theta = n * ((75 * Math.PI) / 180);
ballSpeedY = ballSpeed * Math.sin(theta);
}
function moveBall() {
let length = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
let scale = ballSpeed / length;
ballX += (ballSpeedX * scale) * elapsed;
ballY += (ballSpeedY * scale) * elapsed;
if (ballY <= 0 || ballY >= canvas.height - ballSize)
ballSpeedY *= -1;
if (ballX <= paddleWidth + paddleOffset && ballX >= paddleOffset &&
ballY > leftPaddleY && ballY < leftPaddleY + paddleHeight)
{
ballSpeedX *= -1;
ballX = paddleWidth + paddleOffset;
getBounceVelocity(leftPaddleY);
ballSpeed += 10;
}
if (ballX >= canvas.width - paddleWidth - ballSize - paddleOffset && ballX <= canvas.width - ballSize - paddleOffset &&
ballY > rightPaddleY && ballY < rightPaddleY + paddleHeight)
{
ballSpeedX *= -1;
ballX = canvas.width - paddleWidth - ballSize - paddleOffset;
getBounceVelocity(rightPaddleY);
ballSpeed += 10;
}
// scoring
if (ballX < 0 || ballX > canvas.width - ballSize)
{
setOnekoState("default");
game_playing = false;
if (ballX < 0)
p2_score++;
else
p1_score++;
if (p1_score === 3 || p2_score === 3)
{
// ------------------------------------------------------------------------------------------------------------------------------------------
//
// insert the fetch to the ScoreStore api here
//
// ------------------------------------------------------------------------------------------------------------------------------------------
match_over = true;
}
else
{
countdown = 3;
countdownTimer = performance.now();
}
ballX = canvas.width / 2;
ballY = canvas.height / 2;
ballSpeed = 200;
ballSpeedX = 300 * ((ballSpeedX > 0) ? 1 : -1);
ballSpeedY = 10;
ballSpeedX = -ballSpeedX;
leftPaddleY = canvas.height / 2 - paddleHeight / 2;
rightPaddleY = canvas.height / 2 - paddleHeight / 2;
}
setBallPos(ballX, ballY);
}
function draw() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = "white";
ctx.beginPath();
ctx.setLineDash([5, 10]);
ctx.moveTo(canvas.width / 2, 0);
ctx.lineTo(canvas.width / 2, canvas.height);
ctx.stroke();
ctx.fillStyle = "white";
ctx.fillRect(paddleOffset, leftPaddleY, paddleWidth, paddleHeight);
ctx.fillRect(canvas.width - paddleWidth - paddleOffset, rightPaddleY, paddleWidth, paddleHeight);
ctx.fillStyle = "white";
if (game_playing)
ctx.fillRect(ballX, ballY, ballSize, ballSize);
ctx.font = "24px Kubasta";
let text_score = `${p1_score} - ${p2_score}`;
ctx.fillText(text_score, canvas.width / 2 - (ctx.measureText(text_score).width / 2), 25);
ctx.fillText(p1_name, canvas.width / 4 - (ctx.measureText(p1_name).width / 2), 45);
ctx.fillText(p2_name, (canvas.width / 4 * 3) - (ctx.measureText(p2_name).width / 2), 45);
if (match_over)
{
ctx.font = "32px Kubasta";
const winner = `${p1_score > p2_score ? p1_name : p2_name} won :D`;
ctx.fillText(winner, canvas.width / 2 - (ctx.measureText(winner).width / 2), canvas.height / 2 + 16);
document.getElementById("game-buttons").classList.remove("hidden");
}
}
function startCountdown()
{
const now = performance.now();
if (countdown > 0)
{
if (now - countdownTimer >= 500)
{
countdown--;
countdownTimer = now;
}
ctx.font = "48px Kubasta";
ctx.fillText(countdown.toString(), canvas.width / 2 - 10, canvas.height / 2 + 24);
}
else if (countdown === 0)
{
ctx.font = "48px Kubasta";
ctx.fillText("Go!", canvas.width / 2 - 30, canvas.height / 2 + 24);
setTimeout(() => {
game_playing = true;
countdown = -1;
}, 500);
}
}
const gameLoop = (timestamp: number) => {
elapsed = (timestamp - start) / 1000;
start = timestamp;
if (game_playing)
{
movePaddles();
moveBall();
}
draw();
if (!game_playing)
startCountdown();
if (this.running)
requestAnimationFrame(gameLoop);
};
document.getElementById("game-retry")?.addEventListener("click", () => {
setOnekoState("pong");
document.getElementById("game-buttons").classList.add("hidden");
game_playing = false;
match_over = false;
p1_score = 0;
p2_score = 0;
countdown = 3;
countdownTimer = performance.now();
});
let p1_input = document.getElementById("player1");
let p2_input = document.getElementById("player2");
p2_input.value = "Player 2";
if (await isLogged())
p1_input.value = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
else
p1_input.value = "Player 1";
document.getElementById("game-start")?.addEventListener("click", () => {
p1_name = p1_input.value;
p2_name = p2_input.value;
document.getElementById("player-inputs").remove();
canvas = document.createElement("canvas");
canvas.id = "gameCanvas";
canvas.classList.add("reverse-border");
document.getElementById("main-div").prepend(canvas);
ctx = canvas.getContext("2d");
ctx.canvas.width = 600;
ctx.canvas.height = 600;
leftPaddleY = canvas.height / 2 - paddleHeight / 2;
rightPaddleY = canvas.height / 2 - paddleHeight / 2;
ballX = canvas.width / 2;
ballY = canvas.height / 2;
setOnekoState("pong");
setOnekoOffset();
requestAnimationFrame(gameLoop);
});
}
}

View File

@ -0,0 +1,80 @@
import Aview from "./Aview.ts"
import { dragElement } from "./drag.js"
import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts"
export default class extends Aview {
constructor()
{
super();
this.setTitle("login");
setOnekoState("default");
}
async getHTML() {
return `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">login.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<form method="dialog" class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome back ! please login.</h1>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
</br>
<button id="login-button" type="submit" class="default-button w-full">login</button>
</form>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
const login = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value;
try {
const data_req = await fetch("http://localhost:3001/login", {
method: "POST",
headers: { "Content-Type": "application/json", },
credentials: "include",
body: JSON.stringify({ user: username, password: password }),
});
const data = await data_req.json();
if (data_req.status === 200)
{
isLogged();
navigationManager("/");
}
else if (data_req.status === 400)
{
document.getElementById("login-error-message").innerHTML = "error: " + data.error;
document.getElementById("login-error-message").classList.remove("hidden");
}
else
{
throw new Error("invalid response");
}
}
catch (error)
{
console.error(error);
document.getElementById("login-error-message").innerHTML = "error: server error, try again later...";
document.getElementById("login-error-message").classList.remove("hidden");
}
};
document.getElementById("login-button")?.addEventListener("click", login);
}
}

View File

@ -0,0 +1,34 @@
import Aview from "./Aview.ts"
import { setOnekoState } from "../oneko.ts"
export default class extends Aview {
constructor()
{
super();
this.setTitle("knl is trans(cendence)");
setOnekoState("default");
}
async getHTML() {
// <div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4"-->
return `
<!--div class="default-border">
<div class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">knl_meowscendence</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pb-4">i like pong</p>
<a class="default-button" href="/pong" data-link>
Pong
</a>
</div>
</div-->
`;
}
}

View File

@ -0,0 +1,42 @@
import Aview from "./Aview.ts"
import { dragElement } from "./drag.js"
import { setOnekoState } from "../oneko.ts"
export default class extends Aview {
constructor()
{
super();
this.setTitle("knl is trans(cendence)");
setOnekoState("default");
}
async getHTML() {
return `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">pong_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome to pong!! Oo</p>
<div class="flex flex-col space-y-4">
<a class="default-button" href="/pong/local" data-link>
local match
</a>
<a class="default-button" href="/pong/tournament" data-link>
local tournament
</a>
</div>
</div>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
}
}

View File

@ -0,0 +1,105 @@
import Aview from "./Aview.ts"
import { isLogged, navigationManager } from "../main.ts"
export default class extends Aview {
constructor()
{
super();
this.setTitle("profile");
}
async getHTML() {
return `
<div id="main-window" class="default-border shadow-2x1 bg-neutral-200 dark:bg-neutral-800">
<div class="flex flex-row items-stretch">
<div class="inline-block bg-linear-to-b from-orange-200 to-orange-300 min-h-84 w-6 relative">
<!--div class="absolute bottom-1 left-full whitespace-nowrap origin-bottom-left -rotate-90 font-bold">knl_meowscendence</div-->
<div class="absolute bottom-1 left-full whitespace-nowrap origin-bottom-left -rotate-90 font-bold">girls kissing :3</div>
</div>
<div class="flex flex-col items-center">
<div id="profile-items" class="flex flex-col items-center">
</div>
<div id="menu-bottom-div" class="hidden mt-auto flex flex-col items-center">
<hr class="my-2 w-32 reverse-border">
<button id="menu-logout" class="menu-default-button">logout</button>
</div>
</div>
</div>
</div>
`;
}
open: boolean = false;
async run() {
let uuid: String;
if (this.open)
{
this.open = false;
document.getElementById("taskbar-menu").innerHTML = "";
return ;
}
this.open = true;
document.getElementById("taskbar-menu").innerHTML = await this.getHTML();
async function getMainHTML() {
if (!(await isLogged()))
{
document.getElementById("menu-bottom-div").classList.add("hidden");
return `
<a class="menu-default-button inline-flex items-center justify-center" href="/login" data-link>login</a>
<a class="menu-default-button inline-flex items-center justify-center" href="/register" data-link>register</a>
`;
}
document.getElementById("menu-bottom-div").classList.remove("hidden");
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
return `
<span class="menu-default-label inline-flex items-center justify-center">hi, ${uuid} !</span>
<hr class="my-2 w-32 reverse-border">
<button class="menu-default-button">profile</button>
<button class="menu-default-button">settings</button>
`;
}
document.getElementById("profile-items").innerHTML = await getMainHTML();
/*const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, {
method: "GET",
credentials: "include",
});
if (userdata_req.status == 404)
{
console.error("invalid user");
return ;
}
let userdata = await userdata_req.json();
console.log(userdata_req);*/
/*const main = document.getElementById("profile-profile");
const nametag = main.appendChild(document.createElement("span"));
nametag.innerHTML = `Hiiiiii ${userdata.displayName} ! :D`;
nametag.classList.add("text-neutral-900", "dark:text-white");
const winrate = main.appendChild(document.createElement("div"));
winrate.innerHTML = `wins: ${userdata.wins} | losses: ${userdata.losses} | winrate: ${userdata.wins / (userdata.wins + userdata.losses)}`;
winrate.classList.add("text-neutral-900", "dark:text-white");*/
//console.log(document.getElementById("menu-logout"));
document.getElementById("menu-logout").addEventListener("click", async () => {
let req = await fetch("http://localhost:3001/logout", {
method: "GET",
credentials: "include",
});
if (req.status === 200)
this.run();
else
console.error("logout failed");
});
}
}

View File

@ -0,0 +1,87 @@
import Aview from "./Aview.ts"
import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts"
import { dragElement } from "./drag.ts";
export default class extends Aview {
constructor()
{
super();
this.setTitle("register");
setOnekoState("default");
}
async getHTML() {
return `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">register.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<form method="dialog" class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome ! please register.</p>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
</br>
<button id="register-button" type="submit" class="default-button w-full">register</button>
</form>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
const login = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value;
try {
const data_req = await fetch("http://localhost:3001/register", {
method: "POST",
headers: { "Content-Type": "application/json", },
credentials: "include",
body: JSON.stringify({ user: username, password: password }),
});
const data = await data_req.json();
if (data_req.status === 200)
{
let uuid_req = await fetch("http://localhost:3001/me", {
method: "GET",
credentials: "include",
});
let uuid = await uuid_req.json();
document.cookie = `uuid=${uuid.user};max-ages=${60*60*24*7}`;
console.log(document.cookie);
isLogged();
navigationManager("/");
}
else if (data_req.status === 400)
{
document.getElementById("login-error-message").innerHTML = "error: " + data.error;
document.getElementById("login-error-message").classList.remove("hidden");
}
else
{
throw new Error("invalid response");
}
}
catch (error)
{
console.error(error);
document.getElementById("login-error-message").innerHTML = "error: server error, try again later...";
document.getElementById("login-error-message").classList.remove("hidden");
}
};
document.getElementById("register-button")?.addEventListener("click", login);
}
}

View File

@ -0,0 +1,706 @@
import Aview from "./Aview.ts";
import { dragElement } from "./drag.js";
export default class extends Aview {
running: boolean;
constructor() {
super();
this.setTitle("tetris (local match)");
this.running = true;
}
async getHTML() {
return `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">pong_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 space-y-4 reverse-border">
<div class="flex flex-row justify-center items-start space-x-4">
<canvas id="hold" class="reverse-border" width="140" heigth="80"></canvas>
<canvas id="board" class="reverse-border" width="300" height="600"></canvas>
<canvas id="queue" class="reverse-border" width="140" height="420"></canvas>
</div>
<div id="game-buttons" class="hidden flex">
<button id="game-retry" class="default-button">play again</button>
<a id="game-back" class="default-button" href="/tetris" data-link>back</a>
</div>
</div>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
const COLS = 10;
const ROWS = 20;
const BLOCK = 30; // pixels per block
type Cell = number; // 0 empty, >0 occupied (color index)
// Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix
const TETROMINOES: { [key: string]: number[][][] } = {
I: [
[
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
],
[
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
],
[
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
],
],
J: [
[
[2, 0, 0],
[2, 2, 2],
[0, 0, 0],
],
[
[0, 2, 2],
[0, 2, 0],
[0, 2, 0],
],
[
[0, 0, 0],
[2, 2, 2],
[0, 0, 2],
],
[
[0, 2, 0],
[0, 2, 0],
[2, 2, 0],
],
],
L: [
[
[0, 0, 3],
[3, 3, 3],
[0, 0, 0],
],
[
[0, 3, 0],
[0, 3, 0],
[0, 3, 3],
],
[
[0, 0, 0],
[3, 3, 3],
[3, 0, 0],
],
[
[3, 3, 0],
[0, 3, 0],
[0, 3, 0],
],
],
O: [
[
[4, 4],
[4, 4],
],
],
S: [
[
[0, 5, 5],
[5, 5, 0],
[0, 0, 0],
],
[
[0, 5, 0],
[0, 5, 5],
[0, 0, 5],
],
[
[0, 0, 0],
[0, 5, 5],
[5, 5, 0],
],
[
[5, 0, 0],
[5, 5, 0],
[0, 5, 0],
],
],
T: [
[
[0, 6, 0],
[6, 6, 6],
[0, 0, 0],
],
[
[0, 6, 0],
[0, 6, 6],
[0, 6, 0],
],
[
[0, 0, 0],
[6, 6, 6],
[0, 6, 0],
],
[
[0, 6, 0],
[6, 6, 0],
[0, 6, 0],
],
],
Z: [
[
[7, 7, 0],
[0, 7, 7],
[0, 0, 0],
],
[
[0, 0, 7],
[0, 7, 7],
[0, 7, 0],
],
[
[0, 0, 0],
[7, 7, 0],
[0, 7, 7],
],
[
[0, 7, 0],
[7, 7, 0],
[7, 0, 0],
],
],
};
const COLORS = [
"#000000", // placeholder for 0
"#00ffff", // I - cyan
"#0000ff", // J - blue
"#ff7f00", // L - orange
"#ffff00", // O - yellow
"#00ff00", // S - green
"#800080", // T - purple
"#ff0000", // Z - red
];
class Piece {
shape: number[][];
rotations: number[][][];
rotationIndex: number;
x: number;
y: number;
colorIndex: number;
constructor(public type: string) {
this.rotations = TETROMINOES[type];
this.rotationIndex = 0;
this.shape = this.rotations[this.rotationIndex];
this.colorIndex = this.findColorIndex();
this.x = Math.floor((COLS - this.shape[0].length) / 2);
this.y = -2; //start on tiles 21 and 22
}
findColorIndex() {
for (const row of this.shape) for (const v of row) if (v) return v;
return 1;
}
rotateCW() {
this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length;
this.shape = this.rotations[this.rotationIndex];
}
rotateCCW() {
this.rotationIndex =
(this.rotationIndex - 1 + this.rotations.length) %
this.rotations.length;
this.shape = this.rotations[this.rotationIndex];
}
getCells(): { x: number; y: number; val: number }[] {
const cells: { x: number; y: number; val: number }[] = [];
for (let r = 0; r < this.shape.length; r++) {
for (let c = 0; c < this.shape[r].length; c++) {
const val = this.shape[r][c];
if (val) cells.push({ x: this.x + c, y: this.y + r, val });
}
}
return cells;
}
}
class Game {
board: Cell[][];
canvas: HTMLCanvasElement | null;
holdCanvas: HTMLCanvasElement | null;
queueCanvas: HTMLCanvasElement | null;
ctx: CanvasRenderingContext2D;
holdCtx: CanvasRenderingContext2D;
queueCtx: CanvasRenderingContext2D;
piece: Piece | null = null;
holdPiece: Piece | null = null;
canHold: boolean = true;
nextQueue: string[] = [];
score = 0;
level = 1;
lines = 0;
dropInterval = 1000;
lastDrop = 0;
isGameOver = false;
isPaused = false;
constructor(canvasId: string) {
const el = document.getElementById(
canvasId,
) as HTMLCanvasElement | null;
this.canvas = el;
this.canvas.width = COLS * BLOCK;
this.canvas.height = ROWS * BLOCK;
const ctx = this.canvas.getContext("2d");
this.ctx = ctx;
this.holdCanvas = document.getElementById("hold");
this.queueCanvas = document.getElementById("queue");
this.holdCtx = this.holdCanvas.getContext("2d");
this.queueCtx = this.queueCanvas.getContext("2d");
this.board = this.createEmptyBoard();
this.fillBag();
this.spawnPiece();
this.registerListeners();
requestAnimationFrame(this.loop.bind(this));
}
createEmptyBoard(): Cell[][] {
const b: Cell[][] = [];
for (let r = 0; r < ROWS; r++) {
const row: Cell[] = new Array(COLS).fill(0);
b.push(row);
}
return b;
}
fillBag() {
// classic 7-bag randomizer
const pieces = Object.keys(TETROMINOES);
const bag = [...pieces];
for (let i = bag.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[bag[i], bag[j]] = [bag[j], bag[i]];
}
this.nextQueue.push(...bag);
}
hold() {
if (!this.canHold) return;
[this.piece, this.holdPiece] = [this.holdPiece, this.piece];
if (!this.piece) this.spawnPiece();
this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2);
this.piece.y = -2;
this.piece.rotationIndex = 0;
this.piece.shape = this.piece.rotations[this.piece.rotationIndex];
this.canHold = false;
this.drawHold();
}
spawnPiece() {
this.canHold = true;
if (this.nextQueue.length < 7) this.fillBag();
const type = this.nextQueue.shift()!;
this.piece = new Piece(type);
// If spawn collides immediately -> game over
if (this.collides(this.piece)) {
this.isGameOver = true;
}
this.drawHold();
this.drawQueue();
}
collides(piece: Piece): boolean {
for (const cell of piece.getCells()) {
if (cell.y >= ROWS) return true;
if (cell.x < 0 || cell.x >= COLS) return true;
if (cell.y >= 0 && this.board[cell.y][cell.x]) return true;
}
return false;
}
getGhostOffset(piece: Piece): number {
let y: number = 0;
while (true) {
for (const cell of piece.getCells()) {
console.log(cell.y + y);
if (
cell.y + y >= ROWS ||
(cell.y + y >= 0 && this.board[cell.y + y][cell.x])
)
return y - 1;
}
y++;
}
}
lockPiece() {
if (!this.piece) return;
let isValid: boolean = false;
for (const cell of this.piece.getCells()) {
if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS)
this.board[cell.y][cell.x] = cell.val;
if (cell.y < 20) isValid = true;
}
if (!isValid) this.isGameOver = true;
this.clearLines();
this.spawnPiece();
}
clearLines() {
let linesCleared = 0;
outer: for (let r = ROWS - 1; r >= 0; r--) {
for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer;
this.board.splice(r, 1);
this.board.unshift(new Array(COLS).fill(0));
linesCleared++;
r++;
}
if (linesCleared > 0) {
this.lines += linesCleared;
// scoring like classic tetris
const points = [0, 40, 100, 300, 1200];
this.score += (points[linesCleared] || 0) * this.level;
// level up every 10 lines (Fixed Goal System)
const newLevel = Math.floor(this.lines / 10) + 1;
if (newLevel > this.level) {
this.level = newLevel;
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75);
}
}
}
rotatePiece(dir: "cw" | "ccw") {
if (!this.piece) return;
// Try rotation with wall kicks
const originalIndex = this.piece.rotationIndex;
if (dir === "cw") this.piece.rotateCW();
else this.piece.rotateCCW();
const kicks = [0, -1, 1, -2, 2];
for (const k of kicks) {
this.piece.x += k;
if (!this.collides(this.piece)) return;
this.piece.x -= k;
}
// no valid kick, revert
this.piece.rotationIndex = originalIndex;
this.piece.shape = this.piece.rotations[originalIndex];
}
movePiece(dx: number, dy: number) {
if (!this.piece) return;
this.piece.x += dx;
this.piece.y += dy;
if (this.collides(this.piece)) {
this.piece.x -= dx;
this.piece.y -= dy;
return false;
}
return true;
}
hardDrop() {
if (!this.piece) return;
let dropped = 0;
while (this.movePiece(0, 1)) dropped++;
this.score += dropped * 2;
this.lockPiece();
}
softDrop() {
if (!this.piece) return;
if (!this.movePiece(0, 1)) return;
//this.lockPiece();
else this.score += 1;
}
keys: Record<string, boolean> = {};
registerListeners() {
window.addEventListener("keydown", (e) => {
this.keys[e.key] = true;
if (this.isGameOver) return;
if (e.key === "p" || e.key === "P" || e.key === "Escape")
this.isPaused = !this.isPaused;
if (this.isPaused) return;
if (e.key === "ArrowLeft") this.movePiece(-1, 0);
else if (e.key === "ArrowRight") this.movePiece(1, 0);
else if (e.key === "ArrowDown") this.softDrop();
else if (e.code === "Space") {
e.preventDefault();
this.hardDrop();
} else if (e.code === "ShiftLeft") {
e.preventDefault();
this.hold();
} else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp")
this.rotatePiece("cw");
else if (e.key === "z" || e.key === "Z" || e.code === "ControlLeft")
this.rotatePiece("ccw");
});
document.addEventListener("keyup", (e) => {
this.keys[e.key] = false;
});
}
loop(timestamp: number) {
if (!this.lastDrop) this.lastDrop = timestamp;
if (!this.isPaused && !this.isGameOver) {
if (timestamp - this.lastDrop > this.dropInterval) {
if (!this.movePiece(0, 1)) this.lockPiece();
this.lastDrop = timestamp;
}
}
this.draw();
requestAnimationFrame(this.loop.bind(this));
}
drawGrid() {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.strokeStyle = "#222";
for (let r = 0; r <= ROWS; r++) {
// horizontal lines
ctx.beginPath();
ctx.moveTo(0, r * BLOCK);
ctx.lineTo(COLS * BLOCK, r * BLOCK);
ctx.stroke();
}
for (let c = 0; c <= COLS; c++) {
ctx.beginPath();
ctx.moveTo(c * BLOCK, 0);
ctx.lineTo(c * BLOCK, ROWS * BLOCK);
ctx.stroke();
}
}
drawBoard() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const val = this.board[r][c];
if (val) this.fillBlock(c, r, COLORS[val]);
else this.clearBlock(c, r);
}
}
}
drawPiece() {
if (!this.piece) return;
for (const cell of this.piece.getCells())
if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val]);
let offset: number = this.getGhostOffset(this.piece);
for (const cell of this.piece.getCells())
if (cell.y + offset >= 0)
this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]);
}
drawHold() {
if (!this.holdPiece) return;
this.holdCtx.clearRect(0, 0, 200, 200);
let y: number = 0;
for (const row of this.holdPiece.rotations[0]) {
let x: number = 0;
for (const val of row) {
if (val) {
this.holdCtx.fillStyle = this.canHold
? COLORS[this.holdPiece.findColorIndex()]
: "gray";
this.holdCtx.fillRect(
x * BLOCK +
1 +
(4 - this.holdPiece.rotations[0].length) * 15 +
10,
y * BLOCK + 1 + 20,
BLOCK - 2,
BLOCK - 2,
);
}
x++;
}
y++;
}
}
drawQueue() {
this.queueCtx.clearRect(0, 0, 500, 500);
let placement: number = 0;
console.log(this.nextQueue.slice(0, 5));
for (const nextPiece of this.nextQueue.slice(0, 5)) {
let y: number = 0;
for (const row of TETROMINOES[nextPiece][0]) {
let x: number = 0;
for (const val of row) {
if (val) {
this.queueCtx.fillStyle =
COLORS[
["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1
];
this.queueCtx.fillRect(
x * BLOCK +
1 +
(4 - TETROMINOES[nextPiece][0].length) * 15 +
10,
y * BLOCK +
1 +
placement * 80 +
20 -
(nextPiece === "I" ? 15 : 0),
BLOCK - 2,
BLOCK - 2,
);
}
x++;
}
y++;
}
placement++;
}
}
fillBlock(x: number, y: number, color: string) {
const ctx = this.ctx;
ctx.fillStyle = color;
ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
}
fillGhostBlock(x: number, y: number, color: string) {
const ctx = this.ctx;
ctx.strokeStyle = color;
ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
}
clearBlock(x: number, y: number) {
const ctx = this.ctx;
ctx.clearRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
}
drawHUD() {
const ctx = this.ctx;
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(4, 4, 120, 60);
ctx.fillStyle = "#fff";
ctx.font = "12px Kubasta";
ctx.fillText(`Score: ${this.score}`, 8, 20);
ctx.fillText(`Lines: ${this.lines}`, 8, 36);
ctx.fillText(`Level: ${this.level}`, 8, 52);
if (this.isPaused) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48);
ctx.fillStyle = "#fff";
ctx.font = "24px Kubasta";
ctx.textAlign = "center";
ctx.fillText(
"PAUSED",
this.canvas.width / 2,
this.canvas.height / 2 + 8,
);
ctx.textAlign = "start";
}
if (this.isGameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72);
ctx.fillStyle = "#fff";
ctx.font = "28px Kubasta";
ctx.textAlign = "center";
ctx.fillText(
"GAME OVER",
this.canvas.width / 2,
this.canvas.height / 2 + 8,
);
ctx.textAlign = "start";
}
}
draw() {
// clear everything
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = "#000";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.strokeStyle = "#111";
for (let r = 0; r <= ROWS; r++) {
this.ctx.beginPath();
this.ctx.moveTo(0, r * BLOCK);
this.ctx.lineTo(COLS * BLOCK, r * BLOCK);
this.ctx.stroke();
}
for (let c = 0; c <= COLS; c++) {
this.ctx.beginPath();
this.ctx.moveTo(c * BLOCK, 0);
this.ctx.lineTo(c * BLOCK, ROWS * BLOCK);
this.ctx.stroke();
}
this.drawBoard();
this.drawPiece();
this.drawHUD();
}
}
const game = new Game("board");
}
}

View File

@ -0,0 +1,42 @@
import Aview from "./Aview.ts"
import { dragElement } from "./drag.js"
import { setOnekoState } from "../oneko.ts"
export default class extends Aview {
constructor()
{
super();
this.setTitle("knl is trans(cendence)");
setOnekoState("default");
}
async getHTML() {
return `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">tetris_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome to tetris! :D</p>
<div class="flex flex-col space-y-4">
<a class="default-button" href="/tetris/solo" data-link>
solo game
</a>
<a class="default-button" href="/tetris/versus" data-link>
versus game
</a>
</div>
</div>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
}
}

View File

@ -0,0 +1,143 @@
import Aview from "./Aview.ts"
import { setOnekoState, setBallPos } from "../oneko.ts"
export default class extends Aview {
constructor()
{
super();
this.setTitle("Tournament");
setOnekoState("default");
}
async getHTML() {
return `
<div class="text-center p-12 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4">
<p class="text-gray-700 dark:text-white text-lg font-bold pb-4">how many players ?</p>
<div class="flex flex-col space-y-4">
<input type="number" id="playerNumber" value="6" placeholder="number of players" class="bg-white text-neutral-900 border rounded-md w-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></input>
<button type="submit" id="bracket-generate" class="bg-blue-600 text-white hover:bg-blue-500 w-full py-2 rounded-md transition-colors">create the bracket</button>
<div id="bracket"></div>
</div>
</div>
`;
}
async run() {
const generateBracket = async (playerCount: number) => {
document.getElementById("bracket").innerHTML = "";
const rounds = Math.ceil(Math.log2(playerCount));
const totalSlots = 2 ** rounds;
const byes = totalSlots - playerCount;
let odd = 0;
if (playerCount % 2)
{
console.error("odd numbers are temporarily invalids");
return ;
/*++odd;
--playerCount;*/
}
let notPowPlayersCount = 0;
if ((playerCount & (playerCount - 1)) != 0)
notPowPlayersCount = playerCount - (2 ** Math.floor(Math.log2(playerCount)));
let initialPlayers = Array.from({ length: 2 ** Math.floor(Math.log2(playerCount))}, (_, i) => `Player ${i + 1}`);
playerCount = 2 ** Math.floor(Math.log2(playerCount));
//let initialPlayers = Array.from({ length: playerCount }, (_, i) => `Player ${i + 1}`);
const bracketWrapper = document.createElement("div");
bracketWrapper.className = "flex space-x-8 overflow-x-auto";
// Round 0: Player input column
const playerInputColumn = document.createElement("div");
playerInputColumn.className = `flex flex-col mt-${(notPowPlayersCount + odd) * 28} space-y-4`;
initialPlayers.forEach((name, i) => {
const input = document.createElement("input");
input.type = "text";
input.id = `playerName${i}`;
input.value = "";
input.placeholder = name;
input.className =
"w-32 h-10 p-2 text-sm border rounded bg-white shadow disabled:bg-gray-200";
playerInputColumn.appendChild(input);
});
bracketWrapper.appendChild(playerInputColumn);
let currentRound = initialPlayers;
let previousPadding = 4;
for (let round = 1; round <= rounds; round++)
{
const roundColumn = document.createElement("div");
previousPadding = previousPadding * 2 + 10
roundColumn.className = `flex flex-col justify-center space-y-${previousPadding}`;
const nextRound: string[] = [];
if (!notPowPlayersCount)
{
if (odd)
{
const input = document.createElement("input");
input.type = "text";
input.id = `playerName${playerCount}`;
input.value = "";
input.placeholder = `Player ${++playerCount}`;
input.className =
"w-32 h-10 p-2 text-sm border rounded bg-white shadow disabled:bg-gray-200";
roundColumn.appendChild(input);
odd--;
nextRound.push("");
}
}
while (notPowPlayersCount)
{
const input = document.createElement("input");
input.type = "text";
input.id = `playerName${playerCount}`;
input.value = "";
input.placeholder = `Player ${++playerCount}`;
input.className =
"w-32 h-10 p-2 text-sm border rounded bg-white shadow disabled:bg-gray-200";
roundColumn.appendChild(input);
--notPowPlayersCount;
nextRound.push("");
}
for (let i = 0; i < currentRound.length; i += 2)
{
const p1 = currentRound[i];
const p2 = currentRound[i + 1];
const matchDiv = document.createElement("div");
matchDiv.className =
"w-32 h-10 flex items-center justify-center bg-white border rounded shadow text-center text-sm";
matchDiv.textContent = "";
nextRound.push("");
roundColumn.appendChild(matchDiv);
}
bracketWrapper.appendChild(roundColumn);
currentRound = nextRound;
}
document.getElementById("bracket").appendChild(bracketWrapper);
};
document.getElementById("bracket-generate")?.addEventListener("click", () => {
const input: HTMLInputElement = document.getElementById("playerNumber") as HTMLInputElement;
generateBracket(+input.value);
});
}
}

View File

@ -0,0 +1,43 @@
import { setOnekoOffset } from "../oneko.ts";
export function dragElement(el) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (document.getElementById(el.id + "-header")) {
// if present, the header is where you move the DIV from:
document.getElementById(el.id + "-header").onmousedown = dragMouseDown;
} else {
// otherwise, move the DIV from anywhere inside the DIV:
el.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
el.style.top = (el.offsetTop - pos2) + "px";
el.style.left = (el.offsetLeft - pos1) + "px";
setOnekoOffset();
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
}

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,5 +1,5 @@
export default {
content: ['./src/front/**/*.{html,js}'],
content: ['./src/front/**/*.{html,js,ts,css}'],
theme: {
extend: {},
},