npm workspaces, NodeNext for server
This commit is contained in:
@@ -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%;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
made with ♥ by{" "}
|
||||
<a href="https://nize.ph" target="_blank" rel="noreferrer">
|
||||
nize
|
||||
</a>
|
||||
<br />
|
||||
© {new Date().getFullYear()} —{" "}
|
||||
<a
|
||||
href="https://github.com/nizewn/chessu"
|
||||
className={styles.github}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user