mirror of
https://github.com/KeyZox71/knl_meowscendence.git
synced 2025-10-14 02:54:44 +02:00
「🏗️」 wip(src/front): first try at a basic architecture. game logic + SPA implemented
This commit is contained in:
@ -1,21 +1,28 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>Vite + Tailwind Test</title>
|
||||||
<link href="/style.css" rel="stylesheet" />
|
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
|
|
||||||
<script src="https://kanel.ovh/oneko.js"></script>
|
|
||||||
<script type="module" src="main.tsx"></script>
|
|
||||||
<div class="text-center p-10 bg-white rounded-xl shadow space-y-4">
|
<body class="bg-gray-100 dark:bg-neutral-900 h-screen flex flex-col">
|
||||||
<h1 class="text-4xl font-bold text-blue-600">Vite + Tailwind</h1>
|
<!--script src="https://kanel.ovh/oneko.js"></script-->
|
||||||
<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">
|
<nav class="bg-white dark:bg-neutral-800 mx-8 rounded-b-xl shadow-md px-4 sm:px-6 lg:px-8 flex justify-end h-16 items-center space-x-6 m-">
|
||||||
Click Me
|
<a class="text-neutral-900 hover:text-neutral-700 dark:text-white dark:hover:text-neutral-400" href="/" data-link>home</a>
|
||||||
</button>
|
<a class="text-neutral-900 hover:text-neutral-700 dark:text-white dark:hover:text-neutral-400" href="/login" data-link>login</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="app" class="flex-1 flex items-center justify-center">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/static/ts/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
console.log("test")
|
|
BIN
src/front/static/assets/favicon.ico
Normal file
BIN
src/front/static/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
5
src/front/static/css/style.css
Normal file
5
src/front/static/css/style.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-accent-500: #f55151;
|
||||||
|
}
|
69
src/front/static/ts/main.ts
Normal file
69
src/front/static/ts/main.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/*import MainMenu from "./views/MainMenu.ts";
|
||||||
|
import PongMenu from "./views/PongMenu.ts";
|
||||||
|
|
||||||
|
import LoginPage from "./views/LoginPage.ts";
|
||||||
|
import RegisterPage from "./views/RegisterPage.ts";
|
||||||
|
|
||||||
|
import Game from "./views/Game.ts";*/
|
||||||
|
|
||||||
|
const navigationManager = url => {
|
||||||
|
history.pushState(null, null, url);
|
||||||
|
router();
|
||||||
|
};
|
||||||
|
|
||||||
|
let view;
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
/*{ path: "/", view: MainMenu },
|
||||||
|
|
||||||
|
{ path: "/pong", view: PongMenu },
|
||||||
|
{ path: "/pong/solo", view: Game },
|
||||||
|
|
||||||
|
{ path: "/login", view: LoginPage },
|
||||||
|
{ path: "/register", view: RegisterPage },*/
|
||||||
|
{ path: "/", view: () => import("./views/MainMenu.ts") },
|
||||||
|
|
||||||
|
{ path: "/pong", view: () => import("./views/PongMenu.ts") },
|
||||||
|
{ path: "/pong/solo", view: () => import("./views/Game.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();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("popstate", router);
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
|
document.body.addEventListener("click", e=> {
|
||||||
|
if (e.target.matches("[data-link]"))
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
navigationManager(e.target.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router();
|
||||||
|
});
|
10
src/front/static/ts/views/Aview.ts
Normal file
10
src/front/static/ts/views/Aview.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default class {
|
||||||
|
contructor()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle(title) { document.title = title; }
|
||||||
|
|
||||||
|
async getHTML() { return ""; }
|
||||||
|
async run() { }
|
||||||
|
};
|
214
src/front/static/ts/views/Game.ts
Normal file
214
src/front/static/ts/views/Game.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
running: boolean;
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("pog or pong ? :3");
|
||||||
|
this.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<script type="module" src="static/ts/pong-logic.ts"></script>
|
||||||
|
<div class="text-center p-5 bg-white rounded-xl shadow space-y-4">
|
||||||
|
<canvas id="gameCanvas" class="rounded-md" width="400" height="400"></canvas>
|
||||||
|
<div id="game-buttons" class="hidden flex">
|
||||||
|
<button id="game-retry" class="bg-blue-600 text-white hover:bg-blue-500 w-full mx-4 py-2 rounded-md transition-colors">play again</button>
|
||||||
|
<a id="game-back" class="bg-gray-600 text-white hover:bg-gray-500 w-full mx-4 py-2 rounded-md transition-colors" href="/pong" data-link>back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
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 countdown: number = 3;
|
||||||
|
let countdownTimer: number = 0;
|
||||||
|
|
||||||
|
const canvas = document.getElementById("gameCanvas") as HTMLCanvasElement;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const paddleOffset: number = 15;
|
||||||
|
const paddleHeight: number = 100;
|
||||||
|
const paddleWidth: number = 10;
|
||||||
|
const ballSize: number = 10;
|
||||||
|
|
||||||
|
let leftPaddleY: number = canvas.height / 2 - paddleHeight / 2;
|
||||||
|
let rightPaddleY: number = canvas.height / 2 - paddleHeight / 2;
|
||||||
|
let paddleSpeed: number = 727 * 0.69;
|
||||||
|
let ballX: number = canvas.width / 2;
|
||||||
|
let ballY: number = canvas.height / 2;
|
||||||
|
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"] && leftPaddleY > 0)
|
||||||
|
leftPaddleY -= paddleSpeed * elapsed;
|
||||||
|
if (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)
|
||||||
|
{
|
||||||
|
game_playing = false;
|
||||||
|
if (ballX < 0)
|
||||||
|
p2_score++;
|
||||||
|
else
|
||||||
|
p1_score++;
|
||||||
|
|
||||||
|
if (p1_score === 3 || p2_score === 3)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
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 sans-serif";
|
||||||
|
ctx.fillText(`${p1_score} - ${p2_score}`, canvas.width / 2 - 20, 30);
|
||||||
|
|
||||||
|
if (match_over)
|
||||||
|
{
|
||||||
|
ctx.font = "48px sans-serif";
|
||||||
|
const winner = p1_score > p2_score ? "Player 1" : "Player 2";
|
||||||
|
ctx.fillText(`${winner} won :D`, canvas.width / 2 - 150, canvas.height / 2 + 22);
|
||||||
|
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 sans-serif";
|
||||||
|
ctx.fillText(countdown.toString(), canvas.width / 2 - 10, canvas.height / 2 + 24);
|
||||||
|
}
|
||||||
|
else if (countdown === 0)
|
||||||
|
{
|
||||||
|
ctx.font = "48px sans-serif";
|
||||||
|
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();
|
||||||
|
console.log(game_playing);
|
||||||
|
if (!game_playing)
|
||||||
|
startCountdown();
|
||||||
|
if (this.running)
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById("game-retry")?.addEventListener("click", () => {
|
||||||
|
document.getElementById("game-buttons").classList.add("hidden");
|
||||||
|
game_playing = false;
|
||||||
|
match_over = false;
|
||||||
|
p1_score = 0;
|
||||||
|
p2_score = 0;
|
||||||
|
|
||||||
|
countdown = 3;
|
||||||
|
countdownTimer = performance.now();
|
||||||
|
});
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
|
}
|
||||||
|
}
|
66
src/front/static/ts/views/LoginPage.ts
Normal file
66
src/front/static/ts/views/LoginPage.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("login");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4 flex flex-col">
|
||||||
|
<h1 class="text-4xl font-bold text-blue-600">login</h1>
|
||||||
|
|
||||||
|
<input type="text" id="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"></input>
|
||||||
|
<input type="password" id="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"></input>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<a class="text-gray-400 dark:text-gray-600 underline" href="/register" data-link>
|
||||||
|
register
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const login = async () => {
|
||||||
|
const username = (document.getElementById("username") as HTMLInputElement).value;
|
||||||
|
const password = (document.getElementById("password") as HTMLInputElement).value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*const response = await fetch("https://localhost/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ user: username, password: password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();*/
|
||||||
|
const data = { "error": "invalid password or smth" };
|
||||||
|
const response = { status: 400};
|
||||||
|
|
||||||
|
|
||||||
|
if (response.status === 200)
|
||||||
|
{
|
||||||
|
navigationManager("/");
|
||||||
|
}
|
||||||
|
else if (response.status === 400)
|
||||||
|
{
|
||||||
|
document.getElementById("login-error-message").innerHTML = "error: " + data.error;
|
||||||
|
document.getElementById("login-error-message").classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (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);
|
||||||
|
}
|
||||||
|
}
|
22
src/front/static/ts/views/MainMenu.ts
Normal file
22
src/front/static/ts/views/MainMenu.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("knl is trans(cendence)");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4">
|
||||||
|
<h1 class="text-4xl font-bold text-blue-800 dark:text-blue-300">knl_meowscendence :D</h1>
|
||||||
|
<p class="text-gray-900 dark:text-white text-lg pb-4">i like pong</p>
|
||||||
|
<a class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded" href="/pong" data-link>
|
||||||
|
Pong
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
21
src/front/static/ts/views/PongMenu.ts
Normal file
21
src/front/static/ts/views/PongMenu.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("ponging ur mom");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4">
|
||||||
|
<p class="text-gray-700 dark:text-white text-3xl font-bold pb-4">pong is funny yay</p>
|
||||||
|
<a class="bg-red-500 hover:bg-red-400 text-white px-4 py-2 rounded" href="/pong/solo" data-link>
|
||||||
|
solo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
27
src/front/static/ts/views/RegisterPage.ts
Normal file
27
src/front/static/ts/views/RegisterPage.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("ft_trans 🏳️⚧️");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4 flex flex-col">
|
||||||
|
<h1 class="text-4xl font-bold text-blue-600">register</h1>
|
||||||
|
|
||||||
|
<input type="text" id="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"></input>
|
||||||
|
<input type="password" id="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"></input>
|
||||||
|
|
||||||
|
<button type="submit" class="bg-blue-600 text-white hover:bg-blue-500 w-full py-2 rounded-md transition-colors">register</button>
|
||||||
|
|
||||||
|
<a class="text-gray-400 dark:text-gray-600 underline" href="/login" data-link>
|
||||||
|
i already have an account
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
/*@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;*/
|
|
Reference in New Issue
Block a user