npm workspaces, NodeNext for server

This commit is contained in:
Nathaniel Tampus
2023-02-05 13:49:40 +08:00
parent dc233b2c84
commit bd39cf8c46
49 changed files with 129 additions and 1605 deletions

View File

@@ -1,125 +0,0 @@
.authBox {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
border-radius: 4px;
position: relative;
}
.orRegister {
font-size: 70%;
}
.orRegister a {
color: var(--blue11);
text-decoration: underline;
}
.authBox h3 {
margin-bottom: 1em;
}
.section {
flex: 1;
width: 50%;
min-width: 250px;
padding: 20px;
position: relative;
}
.sectionDisabled {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 100%;
background-color: rgba(0, 0, 0, 0.4);
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
backdrop-filter: blur(1px);
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
}
.sectionLeft {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
/*border-right: 1px solid #eee;*/
}
.sectionRight {
display: flex;
flex-direction: column;
pointer-events: none;
}
.form {
display: flex;
flex-direction: column;
align-items: stretch;
}
.formGroup {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.formLabel {
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
}
.formInput {
background-color: var(--blue4);
box-shadow: 0 0 0 1px var(--blue6);
color: var(--blue12);
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
.formInput:focus {
box-shadow: 0 0 0 1px var(--blue8);
}
.formButton {
border-radius: 4px;
background-color: var(--blue10);
color: var(--blue1);
font-size: 14px;
font-weight: 500;
padding: 8px 16px;
cursor: pointer;
}
.formButton:focus,
.formButton:hover {
background-color: var(--blue11);
}
.oauth {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-top: 10px;
}
.oauthButton {
width: 50%;
background-color: transparent;
cursor: pointer;
display: inline-flex;
}
.oauthButton img {
width: 100%;
}

View File

@@ -1,90 +0,0 @@
import { MouseEvent, useRef, useContext } from "react";
import { SessionContext } from "../../context/session";
import { setGuestSession } from "../../utils/auth";
import styles from "./Auth.module.css";
import GoogleOAuth from "../../assets/oauth_google.png";
import FacebookOAuth from "../../assets/oauth_facebook.png";
// TODO: clean up this component
const Auth = () => {
const guestNameRef = useRef<HTMLInputElement | null>(null);
const session = useContext(SessionContext);
async function handleGuestLogin(e: MouseEvent<HTMLButtonElement>) {
e.preventDefault();
if (!guestNameRef.current || !guestNameRef.current.value) return;
const user = await setGuestSession(guestNameRef.current.value);
if (user) {
session?.setUser(user);
} else {
console.log("guest auth failed");
}
}
return (
<div className={styles.authBox}>
<div className={`${styles.section} ${styles.sectionLeft}`}>
<h3>Guest login</h3>
<form>
<div className={styles.formGroup}>
<label className={styles.formLabel} htmlFor="guest-username">
Username
</label>
<input
type="text"
id="guest-username"
ref={guestNameRef}
className={styles.formInput}
pattern="[a-zA-Z0-9_-]+"
title="_ - and alphanumeric characters only"
required
/>
</div>
<button className={styles.formButton} type="submit" onClick={handleGuestLogin}>
Continue as Guest
</button>
</form>
</div>
<div className={`${styles.section} ${styles.sectionRight}`}>
<div className={styles.sectionDisabled}>coming soon.</div>
<h3>
Sign In{" "}
<span className={styles.orRegister}>
or <a href="#">register</a>
</span>
</h3>
<form className={styles.form}>
<div className={styles.formGroup}>
<label className={styles.formLabel} htmlFor="email">
Username/Email
</label>
<input type="email" id="email" className={styles.formInput} />
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel} htmlFor="password">
Password
</label>
<input type="password" id="password" className={styles.formInput} />
</div>
<button className={styles.formButton} disabled>
Sign In
</button>
</form>
<div className={styles.oauth}>
<button className={styles.oauthButton} disabled>
<img src={GoogleOAuth} alt="Google" />
</button>
<button className={styles.oauthButton} disabled>
<img src={FacebookOAuth} alt="Facebook" />
</button>
</div>
</div>
</div>
);
};
export default Auth;

View File

@@ -1,217 +0,0 @@
import { useContext, useState, useEffect, useReducer } from "react";
import { Chess, Move, Square } from "chess.js";
import { Chessboard } from "react-chessboard";
import { SocketContext } from "../../context/socket";
import { SessionContext } from "../../context/session";
import type { Game } from "@types";
/**
* bug: always on initial position on page load, regardless of game.fen()
but works fine in production without StrictMode rendering the component twice
* */
const Board = () => {
const socket = useContext(SocketContext);
const session = useContext(SessionContext);
const [size, setSize] = useState(400);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [game, _setGame] = useState(new Chess());
const [side, setSide] = useState<"b" | "w" | "s">("s");
const [moveFrom, setMoveFrom] = useState<string | Square>("");
const [rightClickedSquares, setRightClickedSquares] = useState<{
[square: string]: { backgroundColor: string } | undefined;
}>({});
const [optionSquares, setOptionSquares] = useState<{
[square: string]: { background: string; borderRadius?: string };
}>({});
useEffect(() => {
if (socket === null) return;
window.addEventListener("resize", handleResize);
handleResize();
socket.on("receivedLatestGame", (latestGame: Game) => {
if (latestGame.pgn) {
game.loadPgn(latestGame.pgn);
forceUpdate();
}
if (latestGame.black?.id === session?.user.id) {
if (side !== "b") setSide("b");
} else if (latestGame.white?.id === session?.user.id) {
if (side !== "w") setSide("w");
} else if (side !== "s") {
setSide("s");
}
});
socket.on("receivedMove", (m: { from: string; to: string; promotion?: string }) => {
const success = makeMove(m);
if (!success) {
socket.emit("getLatestGame");
}
});
return () => {
window.removeEventListener("resize", handleResize);
socket.off("receivedMove");
socket.off("receivedLatestGame");
};
}, []);
function handleResize() {
const container = document.getElementById("root");
if (!container || !container.offsetWidth) return;
if (container.offsetWidth > 1600) {
setSize(container.offsetWidth * 0.25);
} else if (container.offsetWidth > 700) {
setSize(container.offsetWidth * 0.35);
} else {
setSize(container.offsetWidth - 100);
}
}
function makeMove(m: { from: string; to: string; promotion?: string }) {
try {
const result = game.move(m);
if (result) {
setOptionSquares({
[m.from]: { background: "rgba(255, 255, 0, 0.4)" },
[m.to]: { background: "rgba(255, 255, 0, 0.4)" }
});
return result;
} else {
throw new Error("invalid move");
}
} catch (e) {
setOptionSquares({});
return false;
}
}
function isDraggablePiece({ piece }: { piece: string }) {
if (side === "s") return true;
return piece.startsWith(side);
}
function onDrop(sourceSquare: Square, targetSquare: Square) {
if (side !== game.turn()) return false;
const moveDetails = {
from: sourceSquare,
to: targetSquare,
promotion: "q"
};
const move = makeMove(moveDetails);
if (!move) return false; // illegal move
socket?.emit("sendMove", moveDetails);
return true;
}
function getMoveOptions(square: Square) {
const moves = game.moves({
square,
verbose: true
}) as Move[];
if (moves.length === 0) {
return;
}
const newSquares: {
[square: string]: { background: string; borderRadius?: string };
} = {};
moves.map((move) => {
newSquares[move.to] = {
background:
game.get(move.to as Square) &&
game.get(move.to as Square)?.color !== game.get(square)?.color
? "radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)"
: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
borderRadius: "50%"
};
return move;
});
newSquares[square] = {
background: "rgba(255, 255, 0, 0.4)"
};
setOptionSquares(newSquares);
}
function onPieceDragBegin(_piece: string, sourceSquare: Square) {
if (side !== game.turn()) return;
getMoveOptions(sourceSquare);
}
function onPieceDragEnd() {
setOptionSquares({});
}
function onSquareClick(square: Square) {
setRightClickedSquares({});
if (side !== game.turn()) return;
function resetFirstMove(square: Square) {
setMoveFrom(square);
getMoveOptions(square);
}
// from square
if (!moveFrom) {
resetFirstMove(square);
return;
}
const moveDetails = {
from: moveFrom as Square,
to: square,
promotion: "q"
};
const move = makeMove(moveDetails);
if (!move) {
resetFirstMove(square);
} else {
setMoveFrom("");
socket?.emit("sendMove", moveDetails);
}
}
function onSquareRightClick(square: Square) {
const colour = "rgba(0, 0, 255, 0.4)";
setRightClickedSquares({
...rightClickedSquares,
[square]:
rightClickedSquares[square] && rightClickedSquares[square]?.backgroundColor === colour
? undefined
: { backgroundColor: colour }
});
}
return (
<Chessboard
position={game.fen()}
animationDuration={200}
isDraggablePiece={isDraggablePiece}
boardOrientation={side === "b" ? "black" : "white"}
boardWidth={size}
onPieceDragBegin={onPieceDragBegin}
onPieceDragEnd={onPieceDragEnd}
onPieceDrop={onDrop}
onSquareClick={onSquareClick}
onSquareRightClick={onSquareRightClick}
customSquareStyles={{
...optionSquares,
...rightClickedSquares
}}
/>
);
};
export default Board;

View File

@@ -1,14 +0,0 @@
.footer {
margin-top: 4em;
padding-bottom: 2em;
width: 100%;
font-size: 0.8em;
}
.footer a {
color: var(--blue12);
}
.github {
text-decoration: underline;
}

View File

@@ -1,24 +0,0 @@
import styles from "./Footer.module.css";
const Footer = () => {
return (
<footer className={styles.footer}>
made with &hearts; by{" "}
<a href="https://nize.ph" target="_blank" rel="noreferrer">
nize
</a>
<br />
&copy; {new Date().getFullYear()} {" "}
<a
href="https://github.com/nizewn/chessu"
className={styles.github}
target="_blank"
rel="noreferrer"
>
GitHub
</a>
</footer>
);
};
export default Footer;

View File

@@ -1,15 +0,0 @@
.header {
padding: 1.5em;
font-size: 2.3em;
text-align: center;
}
.title {
color: var(--blue12);
}
.themeToggle {
padding: 0 0.5em;
background: transparent;
color: var(--blue12);
}

View File

@@ -1,47 +0,0 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import styles from "./Header.module.css";
import { SunIcon, MoonIcon } from "@radix-ui/react-icons";
const Header = () => {
const [darkTheme, setDarkTheme] = useState(false);
useEffect(() => {
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => changeTheme(e.matches ? "dark" : "light"));
changeTheme(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
}, []);
function changeTheme(theme: "dark" | "light") {
if (theme === "dark") {
if (!document.body.classList.contains("dark-theme")) {
document.body.classList.add("dark-theme");
}
setDarkTheme(true);
} else {
if (document.body.classList.contains("dark-theme")) {
document.body.classList.remove("dark-theme");
}
setDarkTheme(false);
}
}
return (
<header className={styles.header}>
<Link to="/" className={styles.title}>
chessu
</Link>
<button
className={styles.themeToggle}
type="button"
onClick={() => changeTheme(darkTheme ? "light" : "dark")}
>
{darkTheme ? <SunIcon /> : <MoonIcon />}
</button>
</header>
);
};
export default Header;