」 feat(src/front): taskbar to access profile and settings for user management is done :D and also its very pretty :3

This commit is contained in:
y-syo
2025-10-12 13:56:10 +02:00
parent 26b16749bb
commit 1141fe3159
7 changed files with 857 additions and 768 deletions

View File

@ -12,7 +12,7 @@
<!--body class="bg-gray-100 dark:bg-neutral-950 h-screen flex flex-col"--> <!--body class="bg-gray-100 dark:bg-neutral-950 h-screen flex flex-col"-->
<body class="bg-neutral-950 dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover 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="https://kanel.ovh/oneko.js"></script-->
<!--script src="./static/ts/oneko.js"></script--> <!--script src="./static/ts/oneko.js"></script-->
@ -35,15 +35,22 @@
<div id="app" class="flex-1 flex items-center justify-center"> <div id="app" class="flex-1 flex items-center justify-center">
</div> </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"> <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 px-4 sm:px-6 lg:px-8 flex justify-start h-12 items-center space-x-6 font-[Kubasta]"> <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]">
<a id="profile-button" class="text-neutral-900 hover:text-neutral-700 dark:text-white dark:hover:text-neutral-400" href="/login" data-link>login</a> <div class="flex px-4 items-center content-center space-x-2">
<a class="text-neutral-900 hover:text-neutral-700 dark:text-white dark:hover:text-neutral-400" href="/" data-link>home</a> <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> </nav>
</div> </div>
<script type="module" src="/static/ts/main.ts"></script> <script type="module" src="/static/ts/main.ts"></script>
</body> </body>
</html> </html>

View File

@ -47,3 +47,32 @@
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 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
;
}

View File

@ -1,6 +1,8 @@
import { oneko } from "./oneko.ts"; import { oneko } from "./oneko.ts";
import Profile from "./views/Profile.ts";
let profile_view = new Profile;
export async function isLogged(): boolean { export async function isLogged(): Promise<boolean> {
let uuid_req = await fetch("http://localhost:3001/me", { let uuid_req = await fetch("http://localhost:3001/me", {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
@ -9,63 +11,11 @@ export async function isLogged(): boolean {
{ {
let uuid = await uuid_req.json(); let uuid = await uuid_req.json();
document.cookie = `uuid=${uuid.user};max-age=${60*60*24*7}`; document.cookie = `uuid=${uuid.user};max-age=${60*60*24*7}`;
const old_button = document.getElementById("profile-button");
const dropdown = document.createElement("div");
dropdown.classList.add("relative", "inline-block", "group");
dropdown.id = "profile-button";
const button_dropdown = dropdown.appendChild(document.createElement("button"));
button_dropdown.innerHTML = uuid.user;
button_dropdown.classList.add("text-neutral-900", "group-hover:text-neutral-700", "dark:text-white", "dark:group-hover:text-neutral-400");
const menu_div = dropdown.appendChild(document.createElement("div"));
menu_div.classList.add("float:right", "hidden", "absolute", "left-0", "bottom-full", "dark:bg-neutral-800", "dark:text-white", "min-w-[160px]", "shadow-lg", "z-10", "group-hover:block");
const profile_a = menu_div.appendChild(document.createElement("a"));
const settings_a = menu_div.appendChild(document.createElement("a"));
const logout_button = menu_div.appendChild(document.createElement("button"));
profile_a.text = "profile";
profile_a.classList.add("block", "no-underline", "px-4", "py-3");
profile_a.href = "/profile";
profile_a.setAttribute("data-link", "");
settings_a.text = "settings";
settings_a.classList.add("block", "no-underline", "px-4", "py-3");
settings_a.href = "/settings";
settings_a.setAttribute("data-link", "");
logout_button.innerHTML = "logout";
logout_button.classList.add("block", "no-underline", "px-4", "py-3");
logout_button.id = "logout-button";
//document.getElementById("logout-button")?.addEventListener("click", async () => {
logout_button.addEventListener("click", async () => {
let req = await fetch("http://localhost:3001/logout", {
method: "GET",
credentials: "include",
});
if (req.status === 200)
isLogged();
else
console.error("logout failed");
});
old_button.replaceWith(dropdown);
return true; return true;
} }
else // 401 else // 401
{ {
document.cookie = `uuid=;max-age=0`; document.cookie = `uuid=;max-age=0`;
const old_button = document.getElementById("profile-button");
const login_button = document.createElement("a");
login_button.id = "profile-button";
login_button.text = "login";
login_button.classList.add("text-neutral-900", "hover:text-neutral-700", "dark:text-white", "dark:hover:text-neutral-400");
login_button.href = "/login";
login_button.setAttribute("data-link", "");
old_button.replaceWith(login_button);
return false; return false;
} }
} }
@ -90,8 +40,6 @@ const routes = [
{ path: "/login", view: () => import("./views/LoginPage.ts") }, { path: "/login", view: () => import("./views/LoginPage.ts") },
{ path: "/register", view: () => import("./views/RegisterPage.ts") }, { path: "/register", view: () => import("./views/RegisterPage.ts") },
{ path: "/profile", view: () => import("./views/Profile.ts") },
]; ];
const router = async () => { const router = async () => {
@ -117,12 +65,18 @@ const router = async () => {
view.run(); view.run();
}; };
document.getElementById("profile-button")?.addEventListener("click", () => {profile_view.run();});
window.addEventListener("popstate", router); window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
isLogged(); isLogged();
document.body.addEventListener("click", e=> { 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]")) if (e.target.matches("[data-link]"))
{ {
e.preventDefault(); e.preventDefault();
@ -144,3 +98,21 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
oneko(); 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

@ -1,4 +1,5 @@
import Aview from "./Aview.ts" import Aview from "./Aview.ts"
import { dragElement } from "./drag.js"
import { setOnekoState } from "../oneko.ts" import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts" import { isLogged, navigationManager } from "../main.ts"
@ -13,22 +14,30 @@ export default class extends Aview {
async getHTML() { async getHTML() {
return ` return `
<form method="dialog" class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4 flex flex-col"> <div id="window" class="absolute default-border">
<h1 class="text-4xl font-bold text-blue-600">login</h1> <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>
<input type="text" id="username" placeholder="username" 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" required></input> <form method="dialog" class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 border w-full px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required></input> <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> <p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
<button id="login-button" type="submit" class="bg-blue-600 text-white hover:bg-blue-500 w-full py-2 rounded-md transition-colors">login</button> </br>
<button id="login-button" type="submit" class="default-button w-full">login</button>
<a class="text-gray-400 dark:text-gray-600 underline" href="/register" data-link>
register
</a>
</form> </form>
</div>
`; `;
} }
async run() { async run() {
dragElement(document.getElementById("window"));
const login = async () => { const login = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value; const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value;

View File

@ -6,36 +6,81 @@ export default class extends Aview {
constructor() constructor()
{ {
super(); super();
if (!isLogged())
navigationManager("/login");
this.setTitle("profile"); this.setTitle("profile");
} }
async getHTML() { async getHTML() {
return ` return `
<div id="main-window" class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4"> <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> </div>
`; `;
} }
open: boolean = false;
async run() { async run() {
const uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; let uuid: String;
const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { 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", method: "GET",
credentials: "include", credentials: "include",
}); });
if (userdata_req.status == 404) if (userdata_req.status == 404)
{ {
console.error("invalid user"); console.error("invalid user");
return ; return ;
} }
let userdata = await userdata_req.json(); let userdata = await userdata_req.json();
console.log(userdata_req); console.log(userdata_req);*/
const main = document.getElementById("main-window"); /*const main = document.getElementById("profile-profile");
const nametag = main.appendChild(document.createElement("span")); const nametag = main.appendChild(document.createElement("span"));
nametag.innerHTML = `Hiiiiii ${userdata.displayName} ! :D`; nametag.innerHTML = `Hiiiiii ${userdata.displayName} ! :D`;
@ -44,6 +89,17 @@ export default class extends Aview {
const winrate = main.appendChild(document.createElement("div")); const winrate = main.appendChild(document.createElement("div"));
winrate.innerHTML = `wins: ${userdata.wins} | losses: ${userdata.losses} | winrate: ${userdata.wins / (userdata.wins + userdata.losses)}`; winrate.innerHTML = `wins: ${userdata.wins} | losses: ${userdata.losses} | winrate: ${userdata.wins / (userdata.wins + userdata.losses)}`;
winrate.classList.add("text-neutral-900", "dark:text-white"); 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

@ -1,6 +1,7 @@
import Aview from "./Aview.ts" import Aview from "./Aview.ts"
import { setOnekoState } from "../oneko.ts" import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts" import { isLogged, navigationManager } from "../main.ts"
import { dragElement } from "./drag.ts";
export default class extends Aview { export default class extends Aview {
@ -13,22 +14,30 @@ export default class extends Aview {
async getHTML() { async getHTML() {
return ` return `
<form method="dialog" class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4 flex flex-col"> <div id="window" class="absolute default-border">
<h1 class="text-4xl font-bold text-blue-600">register</h1> <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>
<input type="text" id="username" placeholder="username" 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" required></input> <form method="dialog" class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 border w-full px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required></input> <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> <p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
<button id="register-button" type="submit" class="bg-blue-600 text-white hover:bg-blue-500 w-full py-2 rounded-md transition-colors">register</button> </br>
<button id="register-button" type="submit" class="default-button w-full">register</button>
<a class="text-gray-400 dark:text-gray-600 underline" href="/login" data-link>
i already have an account
</a>
</form> </form>
</div>
`; `;
} }
async run() { async run() {
dragElement(document.getElementById("window"));
const login = async () => { const login = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value; const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value;

View File

@ -1,11 +1,10 @@
import Aview from "./Aview.ts" import Aview from "./Aview.ts";
import { dragElement } from "./drag.js";
export default class extends Aview { export default class extends Aview {
running: boolean; running: boolean;
constructor() constructor() {
{
super(); super();
this.setTitle("tetris (local match)"); this.setTitle("tetris (local match)");
this.running = true; this.running = true;
@ -40,6 +39,7 @@ export default class extends Aview {
} }
async run() { async run() {
dragElement(document.getElementById("window"));
const COLS = 10; const COLS = 10;
const ROWS = 20; const ROWS = 20;
const BLOCK = 30; // pixels per block const BLOCK = 30; // pixels per block
@ -49,127 +49,176 @@ export default class extends Aview {
// Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix // Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix
const TETROMINOES: { [key: string]: number[][][] } = { const TETROMINOES: { [key: string]: number[][][] } = {
I: [ I: [
[[0,0,0,0] [
,[1,1,1,1] [0, 0, 0, 0],
,[0,0,0,0] [1, 1, 1, 1],
,[0,0,0,0]], [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, 1, 0],
,[0,0,1,0]], [0, 0, 1, 0],
[0, 0, 1, 0],
],
[[0,0,0,0] [
,[0,0,0,0] [0, 0, 0, 0],
,[1,1,1,1] [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] [0, 1, 0, 0],
,[0,1,0,0]], [0, 1, 0, 0],
[0, 1, 0, 0],
],
], ],
J: [ J: [
[[2,0,0] [
,[2,2,2] [2, 0, 0],
,[0,0,0]], [2, 2, 2],
[0, 0, 0],
],
[[0,2,2] [
,[0,2,0] [0, 2, 2],
,[0,2,0]], [0, 2, 0],
[0, 2, 0],
],
[[0,0,0] [
,[2,2,2] [0, 0, 0],
,[0,0,2]], [2, 2, 2],
[0, 0, 2],
],
[[0,2,0] [
,[0,2,0] [0, 2, 0],
,[2,2,0]], [0, 2, 0],
[2, 2, 0],
],
], ],
L: [ L: [
[[0,0,3] [
,[3,3,3] [0, 0, 3],
,[0,0,0]], [3, 3, 3],
[0, 0, 0],
],
[[0,3,0] [
,[0,3,0] [0, 3, 0],
,[0,3,3]], [0, 3, 0],
[0, 3, 3],
],
[[0,0,0] [
,[3,3,3] [0, 0, 0],
,[3,0,0]], [3, 3, 3],
[3, 0, 0],
],
[[3,3,0] [
,[0,3,0] [3, 3, 0],
,[0,3,0]], [0, 3, 0],
[0, 3, 0],
],
], ],
O: [ O: [
[[4,4] [
,[4,4]], [4, 4],
[4, 4],
],
], ],
S: [ S: [
[[0,5,5] [
,[5,5,0] [0, 5, 5],
,[0,0,0]], [5, 5, 0],
[0, 0, 0],
],
[[0,5,0] [
,[0,5,5] [0, 5, 0],
,[0,0,5]], [0, 5, 5],
[0, 0, 5],
],
[[0,0,0] [
,[0,5,5] [0, 0, 0],
,[5,5,0]], [0, 5, 5],
[5, 5, 0],
[[5,0,0] ],
,[5,5,0]
,[0,5,0]],
[
[5, 0, 0],
[5, 5, 0],
[0, 5, 0],
],
], ],
T: [ T: [
[[0,6,0] [
,[6,6,6] [0, 6, 0],
,[0,0,0]], [6, 6, 6],
[0, 0, 0],
],
[[0,6,0] [
,[0,6,6] [0, 6, 0],
,[0,6,0]], [0, 6, 6],
[0, 6, 0],
],
[[0,0,0] [
,[6,6,6] [0, 0, 0],
,[0,6,0]], [6, 6, 6],
[0, 6, 0],
],
[[0,6,0] [
,[6,6,0] [0, 6, 0],
,[0,6,0]], [6, 6, 0],
[0, 6, 0],
],
], ],
Z: [ Z: [
[[7,7,0] [
,[0,7,7] [7, 7, 0],
,[0,0,0]], [0, 7, 7],
[0, 0, 0],
],
[[0,0,7] [
,[0,7,7] [0, 0, 7],
,[0,7,0]], [0, 7, 7],
[0, 7, 0],
],
[[0,0,0] [
,[7,7,0] [0, 0, 0],
,[0,7,7]], [7, 7, 0],
[0, 7, 7],
],
[[0,7,0] [
,[7,7,0] [0, 7, 0],
,[7,0,0]], [7, 7, 0],
[7, 0, 0],
],
], ],
}; };
const COLORS = [ const COLORS = [
'#000000', // placeholder for 0 "#000000", // placeholder for 0
'#00ffff', // I - cyan "#00ffff", // I - cyan
'#0000ff', // J - blue "#0000ff", // J - blue
'#ff7f00', // L - orange "#ff7f00", // L - orange
'#ffff00', // O - yellow "#ffff00", // O - yellow
'#00ff00', // S - green "#00ff00", // S - green
'#800080', // T - purple "#800080", // T - purple
'#ff0000', // Z - red "#ff0000", // Z - red
]; ];
class Piece { class Piece {
@ -191,10 +240,7 @@ export default class extends Aview {
} }
findColorIndex() { findColorIndex() {
for (const row of this.shape) for (const row of this.shape) for (const v of row) if (v) return v;
for (const v of row)
if (v)
return v;
return 1; return 1;
} }
@ -204,20 +250,19 @@ export default class extends Aview {
} }
rotateCCW() { rotateCCW() {
this.rotationIndex = (this.rotationIndex - 1 + this.rotations.length) % this.rotations.length; this.rotationIndex =
(this.rotationIndex - 1 + this.rotations.length) %
this.rotations.length;
this.shape = this.rotations[this.rotationIndex]; this.shape = this.rotations[this.rotationIndex];
} }
getCells(): { x: number; y: number; val: number }[] { getCells(): { x: number; y: number; val: number }[] {
const cells: { 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 r = 0; r < this.shape.length; r++) {
{ for (let c = 0; c < this.shape[r].length; c++) {
for (let c = 0; c < this.shape[r].length; c++)
{
const val = this.shape[r][c]; const val = this.shape[r][c];
if (val) if (val) cells.push({ x: this.x + c, y: this.y + r, val });
cells.push({ x: this.x + c, y: this.y + r, val });
} }
} }
return cells; return cells;
@ -226,9 +271,9 @@ export default class extends Aview {
class Game { class Game {
board: Cell[][]; board: Cell[][];
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement | null;
holdCanvas: HTMLCanvasElement; holdCanvas: HTMLCanvasElement | null;
queueCanvas: HTMLCanvasElement; queueCanvas: HTMLCanvasElement | null;
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
holdCtx: CanvasRenderingContext2D; holdCtx: CanvasRenderingContext2D;
queueCtx: CanvasRenderingContext2D; queueCtx: CanvasRenderingContext2D;
@ -245,17 +290,19 @@ export default class extends Aview {
isPaused = false; isPaused = false;
constructor(canvasId: string) { constructor(canvasId: string) {
const el = document.getElementById(canvasId); const el = document.getElementById(
canvasId,
) as HTMLCanvasElement | null;
this.canvas = el; this.canvas = el;
this.canvas.width = COLS * BLOCK; this.canvas.width = COLS * BLOCK;
this.canvas.height = ROWS * BLOCK; this.canvas.height = ROWS * BLOCK;
const ctx = this.canvas.getContext('2d'); const ctx = this.canvas.getContext("2d");
this.ctx = ctx; this.ctx = ctx;
this.holdCanvas = document.getElementById('hold'); this.holdCanvas = document.getElementById("hold");
this.queueCanvas = document.getElementById('queue'); this.queueCanvas = document.getElementById("queue");
this.holdCtx = this.holdCanvas.getContext('2d'); this.holdCtx = this.holdCanvas.getContext("2d");
this.queueCtx = this.queueCanvas.getContext('2d'); this.queueCtx = this.queueCanvas.getContext("2d");
this.board = this.createEmptyBoard(); this.board = this.createEmptyBoard();
this.fillBag(); this.fillBag();
@ -267,8 +314,7 @@ export default class extends Aview {
createEmptyBoard(): Cell[][] { createEmptyBoard(): Cell[][] {
const b: Cell[][] = []; const b: Cell[][] = [];
for (let r = 0; r < ROWS; r++) for (let r = 0; r < ROWS; r++) {
{
const row: Cell[] = new Array(COLS).fill(0); const row: Cell[] = new Array(COLS).fill(0);
b.push(row); b.push(row);
} }
@ -279,8 +325,7 @@ export default class extends Aview {
// classic 7-bag randomizer // classic 7-bag randomizer
const pieces = Object.keys(TETROMINOES); const pieces = Object.keys(TETROMINOES);
const bag = [...pieces]; const bag = [...pieces];
for (let i = bag.length - 1; i > 0; i--) for (let i = bag.length - 1; i > 0; i--) {
{
const j = Math.floor(Math.random() * (i + 1)); const j = Math.floor(Math.random() * (i + 1));
[bag[i], bag[j]] = [bag[j], bag[i]]; [bag[i], bag[j]] = [bag[j], bag[i]];
} }
@ -288,12 +333,10 @@ export default class extends Aview {
} }
hold() { hold() {
if (!this.canHold) if (!this.canHold) return;
return;
[this.piece, this.holdPiece] = [this.holdPiece, this.piece]; [this.piece, this.holdPiece] = [this.holdPiece, this.piece];
if (!this.piece) if (!this.piece) this.spawnPiece();
this.spawnPiece();
this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2); this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2);
this.piece.y = -2; this.piece.y = -2;
@ -306,13 +349,11 @@ export default class extends Aview {
spawnPiece() { spawnPiece() {
this.canHold = true; this.canHold = true;
if (this.nextQueue.length < 7) if (this.nextQueue.length < 7) this.fillBag();
this.fillBag();
const type = this.nextQueue.shift()!; const type = this.nextQueue.shift()!;
this.piece = new Piece(type); this.piece = new Piece(type);
// If spawn collides immediately -> game over // If spawn collides immediately -> game over
if (this.collides(this.piece)) if (this.collides(this.piece)) {
{
this.isGameOver = true; this.isGameOver = true;
} }
@ -321,26 +362,23 @@ export default class extends Aview {
} }
collides(piece: Piece): boolean { collides(piece: Piece): boolean {
for (const cell of piece.getCells()) for (const cell of piece.getCells()) {
{ if (cell.y >= ROWS) return true;
if (cell.y >= ROWS) if (cell.x < 0 || cell.x >= COLS) return true;
return true; if (cell.y >= 0 && this.board[cell.y][cell.x]) 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; return false;
} }
getGhostOffset(piece: Piece): boolean { getGhostOffset(piece: Piece): number {
let y: number = 0; let y: number = 0;
while (true) while (true) {
{ for (const cell of piece.getCells()) {
for (const cell of piece.getCells())
{
console.log(cell.y + y); console.log(cell.y + y);
if ((cell.y + y >= ROWS) || (cell.y + y >= 0 && this.board[cell.y + y][cell.x])) if (
cell.y + y >= ROWS ||
(cell.y + y >= 0 && this.board[cell.y + y][cell.x])
)
return y - 1; return y - 1;
} }
@ -349,18 +387,14 @@ export default class extends Aview {
} }
lockPiece() { lockPiece() {
if (!this.piece) if (!this.piece) return;
return; let isValid: boolean = false;
let isValid:boolean = false; for (const cell of this.piece.getCells()) {
for (const cell of this.piece.getCells())
{
if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS) if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS)
this.board[cell.y][cell.x] = cell.val; this.board[cell.y][cell.x] = cell.val;
if (cell.y < 20) if (cell.y < 20) isValid = true;
isValid = true;
} }
if (!isValid) if (!isValid) this.isGameOver = true;
this.isGameOver = true;
this.clearLines(); this.clearLines();
this.spawnPiece(); this.spawnPiece();
@ -368,11 +402,8 @@ export default class extends Aview {
clearLines() { clearLines() {
let linesCleared = 0; let linesCleared = 0;
outer: for (let r = ROWS - 1; r >= 0; r--) outer: for (let r = ROWS - 1; r >= 0; r--) {
{ for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer;
for (let c = 0; c < COLS; c++)
if (!this.board[r][c])
continue outer;
this.board.splice(r, 1); this.board.splice(r, 1);
this.board.unshift(new Array(COLS).fill(0)); this.board.unshift(new Array(COLS).fill(0));
@ -380,36 +411,30 @@ export default class extends Aview {
r++; r++;
} }
if (linesCleared > 0) if (linesCleared > 0) {
{
this.lines += linesCleared; this.lines += linesCleared;
// scoring like classic tetris // scoring like classic tetris
const points = [0, 40, 100, 300, 1200]; const points = [0, 40, 100, 300, 1200];
this.score += (points[linesCleared] || 0) * this.level; this.score += (points[linesCleared] || 0) * this.level;
// level up every 10 lines (Fixed Goal System) // level up every 10 lines (Fixed Goal System)
const newLevel = Math.floor(this.lines / 10) + 1; const newLevel = Math.floor(this.lines / 10) + 1;
if (newLevel > this.level) if (newLevel > this.level) {
{
this.level = newLevel; this.level = newLevel;
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75); this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75);
} }
} }
} }
rotatePiece(dir: 'cw' | 'ccw') { rotatePiece(dir: "cw" | "ccw") {
if (!this.piece) if (!this.piece) return;
return;
// Try rotation with wall kicks // Try rotation with wall kicks
const originalIndex = this.piece.rotationIndex; const originalIndex = this.piece.rotationIndex;
if (dir === 'cw') if (dir === "cw") this.piece.rotateCW();
this.piece.rotateCW(); else this.piece.rotateCCW();
else
this.piece.rotateCCW();
const kicks = [0, -1, 1, -2, 2]; const kicks = [0, -1, 1, -2, 2];
for (const k of kicks) { for (const k of kicks) {
this.piece.x += k; this.piece.x += k;
if (!this.collides(this.piece)) if (!this.collides(this.piece)) return;
return;
this.piece.x -= k; this.piece.x -= k;
} }
// no valid kick, revert // no valid kick, revert
@ -418,12 +443,10 @@ export default class extends Aview {
} }
movePiece(dx: number, dy: number) { movePiece(dx: number, dy: number) {
if (!this.piece) if (!this.piece) return;
return;
this.piece.x += dx; this.piece.x += dx;
this.piece.y += dy; this.piece.y += dy;
if (this.collides(this.piece)) if (this.collides(this.piece)) {
{
this.piece.x -= dx; this.piece.x -= dx;
this.piece.y -= dy; this.piece.y -= dy;
return false; return false;
@ -432,69 +455,58 @@ export default class extends Aview {
} }
hardDrop() { hardDrop() {
if (!this.piece) if (!this.piece) return;
return;
let dropped = 0; let dropped = 0;
while (this.movePiece(0, 1)) while (this.movePiece(0, 1)) dropped++;
dropped++;
this.score += dropped * 2; this.score += dropped * 2;
this.lockPiece(); this.lockPiece();
} }
softDrop() { softDrop() {
if (!this.piece) if (!this.piece) return;
return; if (!this.movePiece(0, 1)) return;
if (!this.movePiece(0, 1))
return;
//this.lockPiece(); //this.lockPiece();
else else this.score += 1;
this.score += 1;
} }
keys: Record<string, boolean> = {}; keys: Record<string, boolean> = {};
registerListeners() { registerListeners() {
window.addEventListener('keydown', (e) => { window.addEventListener("keydown", (e) => {
this.keys[e.key] = true; this.keys[e.key] = true;
if (this.isGameOver) return; if (this.isGameOver) return;
if (e.key === 'ArrowLeft')
this.movePiece(-1, 0); if (e.key === "p" || e.key === "P" || e.key === "Escape")
else if (e.key === 'ArrowRight') this.isPaused = !this.isPaused;
this.movePiece(1, 0);
else if (e.key === 'ArrowDown') if (this.isPaused) return;
this.softDrop();
else if (e.code === 'Space') 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(); e.preventDefault();
this.hardDrop(); this.hardDrop();
} } else if (e.code === "ShiftLeft") {
else if (e.code === 'ShiftLeft')
{
e.preventDefault(); e.preventDefault();
this.hold(); this.hold();
} } else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp")
else if (e.key === 'x' || e.key === 'X' || e.key === 'ArrowUp') this.rotatePiece("cw");
this.rotatePiece('cw'); else if (e.key === "z" || e.key === "Z" || e.code === "ControlLeft")
else if (e.key === 'z' || e.key === 'Z' || e.code === 'ControlLeft') this.rotatePiece("ccw");
this.rotatePiece('ccw');
else if (e.key === 'p' || e.key === 'P' || e.key === 'Escape')
this.isPaused = !this.isPaused;
}); });
document.addEventListener("keyup", e => { this.keys[e.key] = false; }); document.addEventListener("keyup", (e) => {
this.keys[e.key] = false;
});
} }
loop(timestamp: number) loop(timestamp: number) {
{ if (!this.lastDrop) this.lastDrop = timestamp;
if (!this.lastDrop) if (!this.isPaused && !this.isGameOver) {
this.lastDrop = timestamp; if (timestamp - this.lastDrop > this.dropInterval) {
if (!this.isPaused && !this.isGameOver) if (!this.movePiece(0, 1)) this.lockPiece();
{
if (timestamp - this.lastDrop > this.dropInterval)
{
if (!this.movePiece(0, 1))
this.lockPiece();
this.lastDrop = timestamp; this.lastDrop = timestamp;
} }
} }
@ -505,17 +517,15 @@ export default class extends Aview {
drawGrid() { drawGrid() {
const ctx = this.ctx; const ctx = this.ctx;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.strokeStyle = '#222'; ctx.strokeStyle = "#222";
for (let r = 0; r <= ROWS; r++) for (let r = 0; r <= ROWS; r++) {
{
// horizontal lines // horizontal lines
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, r * BLOCK); ctx.moveTo(0, r * BLOCK);
ctx.lineTo(COLS * BLOCK, r * BLOCK); ctx.lineTo(COLS * BLOCK, r * BLOCK);
ctx.stroke(); ctx.stroke();
} }
for (let c = 0; c <= COLS; c++) for (let c = 0; c <= COLS; c++) {
{
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(c * BLOCK, 0); ctx.moveTo(c * BLOCK, 0);
ctx.lineTo(c * BLOCK, ROWS * BLOCK); ctx.lineTo(c * BLOCK, ROWS * BLOCK);
@ -524,51 +534,48 @@ export default class extends Aview {
} }
drawBoard() { drawBoard() {
for (let r = 0; r < ROWS; r++) for (let r = 0; r < ROWS; r++) {
{ for (let c = 0; c < COLS; c++) {
for (let c = 0; c < COLS; c++)
{
const val = this.board[r][c]; const val = this.board[r][c];
if (val) if (val) this.fillBlock(c, r, COLORS[val]);
this.fillBlock(c, r, COLORS[val]); else this.clearBlock(c, r);
else
this.clearBlock(c, r);
} }
} }
} }
drawPiece() { drawPiece() {
if (!this.piece) if (!this.piece) return;
return;
for (const cell of this.piece.getCells()) for (const cell of this.piece.getCells())
if (cell.y >= 0) if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val]);
this.fillBlock(cell.x, cell.y, COLORS[cell.val]);
let offset:number = this.getGhostOffset(this.piece); let offset: number = this.getGhostOffset(this.piece);
for (const cell of this.piece.getCells()) for (const cell of this.piece.getCells())
if (cell.y + offset >= 0) if (cell.y + offset >= 0)
this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]); this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]);
} }
drawHold() { drawHold() {
if (!this.holdPiece) if (!this.holdPiece) return;
return ;
this.holdCtx.clearRect(0, 0, 200, 200) this.holdCtx.clearRect(0, 0, 200, 200);
let y: number = 0; let y: number = 0;
for (const row of this.holdPiece.rotations[0]) for (const row of this.holdPiece.rotations[0]) {
{ let x: number = 0;
let x:number = 0; for (const val of row) {
for (const val of row) if (val) {
{ this.holdCtx.fillStyle = this.canHold
if (val) ? COLORS[this.holdPiece.findColorIndex()]
{ : "gray";
this.holdCtx.fillStyle = this.canHold ? COLORS[this.holdPiece.findColorIndex()] : "gray";
this.holdCtx.fillRect( this.holdCtx.fillRect(
x * BLOCK + 1 + ((4 - this.holdPiece.rotations[0].length) * 15) + 10, x * BLOCK +
1 +
(4 - this.holdPiece.rotations[0].length) * 15 +
10,
y * BLOCK + 1 + 20, y * BLOCK + 1 + 20,
BLOCK - 2, BLOCK - 2); BLOCK - 2,
BLOCK - 2,
);
} }
x++; x++;
} }
@ -576,26 +583,33 @@ export default class extends Aview {
} }
} }
drawQueue() { drawQueue() {
this.queueCtx.clearRect(0, 0, 500, 500) this.queueCtx.clearRect(0, 0, 500, 500);
let placement:number = 0; let placement: number = 0;
console.log(this.nextQueue.slice(0, 5)) console.log(this.nextQueue.slice(0, 5));
for (const nextPiece of this.nextQueue.slice(0, 5)) for (const nextPiece of this.nextQueue.slice(0, 5)) {
{
let y: number = 0; let y: number = 0;
for (const row of TETROMINOES[nextPiece][0]) for (const row of TETROMINOES[nextPiece][0]) {
{ let x: number = 0;
let x:number = 0; for (const val of row) {
for (const val of row) if (val) {
{ this.queueCtx.fillStyle =
if (val) COLORS[
{ ["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1
this.queueCtx.fillStyle = COLORS[['I', 'J', 'L', 'O', 'S', 'T', 'Z'].indexOf(nextPiece) + 1]; ];
this.queueCtx.fillRect( this.queueCtx.fillRect(
x * BLOCK + 1 + ((4 - TETROMINOES[nextPiece][0].length) * 15) + 10, x * BLOCK +
y * BLOCK + 1 + (placement * 80) + 20 - (nextPiece === 'I' ? 15 : 0), 1 +
BLOCK - 2, BLOCK - 2); (4 - TETROMINOES[nextPiece][0].length) * 15 +
10,
y * BLOCK +
1 +
placement * 80 +
20 -
(nextPiece === "I" ? 15 : 0),
BLOCK - 2,
BLOCK - 2,
);
} }
x++; x++;
} }
@ -623,34 +637,40 @@ export default class extends Aview {
drawHUD() { drawHUD() {
const ctx = this.ctx; const ctx = this.ctx;
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(4, 4, 120, 60); ctx.fillRect(4, 4, 120, 60);
ctx.fillStyle = '#fff'; ctx.fillStyle = "#fff";
ctx.font = '12px Kubasta'; ctx.font = "12px Kubasta";
ctx.fillText(`Score: ${this.score}`, 8, 20); ctx.fillText(`Score: ${this.score}`, 8, 20);
ctx.fillText(`Lines: ${this.lines}`, 8, 36); ctx.fillText(`Lines: ${this.lines}`, 8, 36);
ctx.fillText(`Level: ${this.level}`, 8, 52); ctx.fillText(`Level: ${this.level}`, 8, 52);
if (this.isPaused) if (this.isPaused) {
{ ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48);
ctx.fillRect(0, (this.canvas.height / 2) - 24, this.canvas.width, 48); ctx.fillStyle = "#fff";
ctx.fillStyle = '#fff'; ctx.font = "24px Kubasta";
ctx.font = '24px Kubasta'; ctx.textAlign = "center";
ctx.textAlign = 'center'; ctx.fillText(
ctx.fillText('PAUSED', this.canvas.width / 2, this.canvas.height / 2 + 8); "PAUSED",
ctx.textAlign = 'start'; this.canvas.width / 2,
this.canvas.height / 2 + 8,
);
ctx.textAlign = "start";
} }
if (this.isGameOver) if (this.isGameOver) {
{ ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72);
ctx.fillRect(0, (this.canvas.height / 2) - 36, this.canvas.width, 72); ctx.fillStyle = "#fff";
ctx.fillStyle = '#fff'; ctx.font = "28px Kubasta";
ctx.font = '28px Kubasta'; ctx.textAlign = "center";
ctx.textAlign = 'center'; ctx.fillText(
ctx.fillText('GAME OVER', this.canvas.width / 2, this.canvas.height / 2 + 8); "GAME OVER",
ctx.textAlign = 'start'; this.canvas.width / 2,
this.canvas.height / 2 + 8,
);
ctx.textAlign = "start";
} }
} }
@ -658,19 +678,17 @@ export default class extends Aview {
// clear everything // clear everything
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = '#000'; this.ctx.fillStyle = "#000";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.strokeStyle = '#111'; this.ctx.strokeStyle = "#111";
for (let r = 0; r <= ROWS; r++) for (let r = 0; r <= ROWS; r++) {
{
this.ctx.beginPath(); this.ctx.beginPath();
this.ctx.moveTo(0, r * BLOCK); this.ctx.moveTo(0, r * BLOCK);
this.ctx.lineTo(COLS * BLOCK, r * BLOCK); this.ctx.lineTo(COLS * BLOCK, r * BLOCK);
this.ctx.stroke(); this.ctx.stroke();
} }
for (let c = 0; c <= COLS; c++) for (let c = 0; c <= COLS; c++) {
{
this.ctx.beginPath(); this.ctx.beginPath();
this.ctx.moveTo(c * BLOCK, 0); this.ctx.moveTo(c * BLOCK, 0);
this.ctx.lineTo(c * BLOCK, ROWS * BLOCK); this.ctx.lineTo(c * BLOCK, ROWS * BLOCK);
@ -683,17 +701,6 @@ export default class extends Aview {
} }
} }
window.addEventListener('load', () => { const game = new Game("board");
try {
const canvas = document.getElementById('board');
if (!canvas) {
console.error('D:');
return;
}
const game = new Game('board');
} catch (err) {
console.error(err);
}
});
} }
} }