diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dc86968 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NODE_ENV=development +PORT=5000 diff --git a/.gitignore b/.gitignore index a547bf3..50c8dda 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env diff --git a/client/package.json b/client/package.json index 852e1a7..f74e718 100644 --- a/client/package.json +++ b/client/package.json @@ -9,8 +9,12 @@ "preview": "vite preview" }, "dependencies": { + "localforage": "^1.10.0", + "match-sorter": "^6.3.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.14.2", + "sort-by": "^1.2.0" }, "devDependencies": { "@types/react": "^18.0.28", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 378b45e..b5f473b 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -5,12 +5,24 @@ settings: excludeLinksFromLockfile: false dependencies: + localforage: + specifier: ^1.10.0 + version: 1.10.0 + match-sorter: + specifier: ^6.3.1 + version: 6.3.1 react: specifier: ^18.2.0 version: 18.2.0 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-router-dom: + specifier: ^6.14.2 + version: 6.14.2(react-dom@18.2.0)(react@18.2.0) + sort-by: + specifier: ^1.2.0 + version: 1.2.0 devDependencies: '@types/react': @@ -45,6 +57,13 @@ packages: engines: {node: '>=10'} dev: true + /@babel/runtime@7.22.6: + resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: false + /@esbuild/android-arm64@0.17.14: resolution: {integrity: sha512-eLOpPO1RvtsP71afiFTvS7tVFShJBCT0txiv/xjFBo5a7R7Gjw7X0IgIaFoLKhqXYAXhahoXm7qAmRXhY4guJg==} engines: {node: '>=12'} @@ -298,6 +317,11 @@ packages: fastq: 1.15.0 dev: true + /@remix-run/router@1.7.2: + resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==} + engines: {node: '>=14'} + dev: false + /@swc/core-darwin-arm64@1.3.44: resolution: {integrity: sha512-Y+oVsCjXUPvr3D9YLuB1gjP84TseM/CRkbPNrf+3JXQhsPEkgxdIdFP1cl/obeqMQrRgPpvSfK+TOvGuOuV22g==} engines: {node: '>=10'} @@ -668,6 +692,10 @@ packages: function-bind: 1.1.1 dev: true + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -718,6 +746,12 @@ packages: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false + /lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -727,6 +761,12 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -734,6 +774,13 @@ packages: js-tokens: 4.0.0 dev: false + /match-sorter@6.3.1: + resolution: {integrity: sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==} + dependencies: + '@babel/runtime': 7.22.6 + remove-accents: 0.4.2 + dev: false + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -791,6 +838,11 @@ packages: engines: {node: '>= 6'} dev: true + /object-path@0.6.0: + resolution: {integrity: sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==} + engines: {node: '>=0.8.0'} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -909,6 +961,29 @@ packages: scheduler: 0.23.0 dev: false + /react-router-dom@6.14.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.7.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.14.2(react@18.2.0) + dev: false + + /react-router@6.14.2(react@18.2.0): + resolution: {integrity: sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.7.2 + react: 18.2.0 + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -929,6 +1004,14 @@ packages: picomatch: 2.3.1 dev: true + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: false + + /remove-accents@0.4.2: + resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} + dev: false + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -972,6 +1055,12 @@ packages: loose-envify: 1.4.0 dev: false + /sort-by@1.2.0: + resolution: {integrity: sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==} + dependencies: + object-path: 0.6.0 + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} diff --git a/client/src/App.tsx b/client/src/App.tsx index 0dfa292..ea7d52c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,43 +1,8 @@ -import wp from "./assets/pieces/wp.png"; -import wn from "./assets/pieces/wn.png"; -import wb from "./assets/pieces/wb.png"; -import wr from "./assets/pieces/wr.png"; -import wq from "./assets/pieces/wq.png"; -import wk from "./assets/pieces/wk.png"; +import Chess, { Move } from "../../server/src/chess/engine"; +import { useState } from "react"; +import ChessBoard from "./components/Chessboard"; -import bp from "./assets/pieces/bp.png"; -import bn from "./assets/pieces/bn.png"; -import bb from "./assets/pieces/bb.png"; -import br from "./assets/pieces/br.png"; -import bq from "./assets/pieces/bq.png"; -import bk from "./assets/pieces/bk.png"; - -import Chess, { - squareColor, - Piece, - PieceType, - Color, - algebraic, - file, - rank, - FILE, - RANK, - squareIndex, - Move, - MOVE_FLAGS, - PIECE_PROMOTION, - COLOR, - PiecePromotionType, -} from "../../server/src/chess/engine"; -import { MouseEventHandler, useLayoutEffect, useState } from "react"; -import Show from "./utils/Show"; - -const PIECES: Record> = { - w: { p: wp, n: wn, b: wb, r: wr, q: wq, k: wk }, - b: { p: bp, n: bn, b: bb, r: br, q: bq, k: bk }, -}; - -export default function App() { +const App = () => { const [chess] = useState(Chess.load()); const [, setUpdate] = useState(false); @@ -58,196 +23,6 @@ export default function App() { ); -} - -// taken form https://github.com/uidotdev/usehooks -function useWindowSize() { - const [size, setSize] = useState({ - width: -1, - height: -1, - }); - - useLayoutEffect(() => { - const handleResize = () => { - setSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - }; - - handleResize(); - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - return size; -} - -const ChessBoard: React.FC<{ - chess: Chess; - sendMove: (move: Move) => void; - blackPerspective?: boolean; -}> = ({ chess, sendMove, blackPerspective }) => { - const { width, height } = useWindowSize(); - const [activeTile, setActiveTile] = useState(-1); - const [promotionMove, setPromotionMove] = useState< - (Pick & { color: Color }) | null - >(null); - - const gridSize = Math.min(width, height, 800); - - const tileProps = new Array(64).fill(null).map((_, i) => ({ - tileNumber: i, - piece: chess.getPiece(i), - isAttacked: false, - isPromotion: false, - isActive: false, - })); - - if (activeTile != -1) { - tileProps[activeTile].isActive = true; - chess.getMovesForSquare(algebraic(activeTile)).forEach(({ to, flags }) => { - const square = squareIndex(to); - tileProps[square].isAttacked = true; - tileProps[square].isPromotion = - flags & MOVE_FLAGS.PROMOTION ? true : false; - }); - } - - const tiles = tileProps.map((props, i) => ( - - )); - - const handleClick: MouseEventHandler = (e) => { - if (promotionMove != null) return; - - // TODO: fix types - // @ts-ignore - const tile = parseInt(e.target.dataset.tile); - - if (isNaN(tile)) return; - - if (activeTile != -1 && tileProps[tile].isAttacked) { - const moveObj = { - from: algebraic(activeTile), - to: algebraic(tile), - color: (chess.getPiece(activeTile) as Piece).color, - }; - - if (tileProps[tile].isPromotion) return setPromotionMove(moveObj); - - sendMove(moveObj); - setActiveTile(-1); - } - - if (tileProps[tile].piece == null) return setActiveTile(-1); - - if (tile == activeTile) setActiveTile(-1); - else setActiveTile(tile); - }; - - const sendPromotionMove = (promotion: PiecePromotionType) => { - if (promotionMove == null) return; - - const moveObj: Move = { - from: promotionMove.from, - to: promotionMove.to, - promotion, - }; - - sendMove(moveObj); - setPromotionMove(null); - setActiveTile(-1); - }; - - return ( -
-
- {blackPerspective == true ? tiles.reverse() : tiles} -
- {promotionMove == null ? ( - <> - ) : ( -
- {PIECE_PROMOTION.map((p) => ( - sendPromotionMove(p)} - /> - ))} -
- )} -
- ); }; -const TILE_COLORS = Object.freeze({ - w: { - bg: "bg-white-tile", - text: "text-black-tile", - active: "bg-sky-400", - }, - b: { - bg: "bg-black-tile", - text: "text-white-tile", - active: "bg-sky-700", - }, -} as const); - -const Tile: React.FC<{ - tileNumber: number; - piece: Piece | null; - isActive: boolean; - isAttacked: boolean; - blackPerspective?: boolean; -}> = ({ tileNumber, piece, isActive, isAttacked, blackPerspective }) => { - const color = squareColor(tileNumber); - - const { bg: bgColor, text: textColor } = TILE_COLORS[color]; - const activeColor = isActive ? TILE_COLORS[color].active : ""; - - const tileFile = file(tileNumber); - const tileRank = rank(tileNumber); - const square = algebraic(tileNumber); - - const isFirstColumn = tileFile == (blackPerspective ? FILE.H : FILE.A); - const isLastRow = tileRank == (blackPerspective ? RANK.EIGHTH : RANK.FIRST); - - return ( -
*]:pointer-events-none ${bgColor} ${activeColor}`} - data-tile={tileNumber} - > - {piece == null ? <> : } - -
- } - > -
- - - -
- {square[1]} -
-
- -
- {square[0]} -
-
- - ); -}; +export default App; diff --git a/client/src/components/Chessboard.tsx b/client/src/components/Chessboard.tsx new file mode 100644 index 0000000..4ff9a15 --- /dev/null +++ b/client/src/components/Chessboard.tsx @@ -0,0 +1,206 @@ +import wp from "../assets/pieces/wp.png"; +import wn from "../assets/pieces/wn.png"; +import wb from "../assets/pieces/wb.png"; +import wr from "../assets/pieces/wr.png"; +import wq from "../assets/pieces/wq.png"; +import wk from "../assets/pieces/wk.png"; + +import bp from "../assets/pieces/bp.png"; +import bn from "../assets/pieces/bn.png"; +import bb from "../assets/pieces/bb.png"; +import br from "../assets/pieces/br.png"; +import bq from "../assets/pieces/bq.png"; +import bk from "../assets/pieces/bk.png"; + +import Chess, { + squareColor, + Piece, + PieceType, + Color, + algebraic, + file, + rank, + FILE, + RANK, + squareIndex, + Move, + MOVE_FLAGS, + PIECE_PROMOTION, + PiecePromotionType, +} from "../../../server/src/chess/engine"; +import { MouseEventHandler, useState } from "react"; +import Show from "../utils/Show"; +import useWindowSize from "../utils/useWindowSize"; + + +const PIECES: Record> = { + w: { p: wp, n: wn, b: wb, r: wr, q: wq, k: wk }, + b: { p: bp, n: bn, b: bb, r: br, q: bq, k: bk }, +}; +const ChessBoard: React.FC<{ + chess: Chess; + sendMove: (move: Move) => void; + blackPerspective?: boolean; +}> = ({ chess, sendMove, blackPerspective }) => { + const { width, height } = useWindowSize(); + const [activeTile, setActiveTile] = useState(-1); + const [promotionMove, setPromotionMove] = useState< + (Pick & { color: Color }) | null + >(null); + + const gridSize = Math.min(width, height, 800); + + const tileProps = new Array(64).fill(null).map((_, i) => ({ + tileNumber: i, + piece: chess.getPiece(i), + isAttacked: false, + isPromotion: false, + isActive: false, + })); + + if (activeTile != -1) { + tileProps[activeTile].isActive = true; + chess.getMovesForSquare(algebraic(activeTile)).forEach(({ to, flags }) => { + const square = squareIndex(to); + tileProps[square].isAttacked = true; + tileProps[square].isPromotion = + flags & MOVE_FLAGS.PROMOTION ? true : false; + }); + } + + const tiles = tileProps.map((props, i) => ( + + )); + + const handleClick: MouseEventHandler = (e) => { + if (promotionMove != null) return; + + // TODO: fix types + // @ts-ignore + const tile = parseInt(e.target.dataset.tile); + + if (isNaN(tile)) return; + + if (activeTile != -1 && tileProps[tile].isAttacked) { + const moveObj = { + from: algebraic(activeTile), + to: algebraic(tile), + color: (chess.getPiece(activeTile) as Piece).color, + }; + + if (tileProps[tile].isPromotion) return setPromotionMove(moveObj); + + sendMove(moveObj); + setActiveTile(-1); + } + + if (tileProps[tile].piece == null) return setActiveTile(-1); + + if (tile == activeTile) setActiveTile(-1); + else setActiveTile(tile); + }; + + const sendPromotionMove = (promotion: PiecePromotionType) => { + if (promotionMove == null) return; + + const moveObj: Move = { + from: promotionMove.from, + to: promotionMove.to, + promotion, + }; + + sendMove(moveObj); + setPromotionMove(null); + setActiveTile(-1); + }; + + return ( +
+
+ {blackPerspective == true ? tiles.reverse() : tiles} +
+ {promotionMove == null ? ( + <> + ) : ( +
+ {PIECE_PROMOTION.map((p) => ( + sendPromotionMove(p)} + /> + ))} +
+ )} +
+ ); +}; + +const TILE_COLORS = Object.freeze({ + w: { + bg: "bg-white-tile", + text: "text-black-tile", + active: "bg-sky-400", + }, + b: { + bg: "bg-black-tile", + text: "text-white-tile", + active: "bg-sky-700", + }, +} as const); + +const Tile: React.FC<{ + tileNumber: number; + piece: Piece | null; + isActive: boolean; + isAttacked: boolean; + blackPerspective?: boolean; +}> = ({ tileNumber, piece, isActive, isAttacked, blackPerspective }) => { + const color = squareColor(tileNumber); + + const { bg: bgColor, text: textColor } = TILE_COLORS[color]; + const activeColor = isActive ? TILE_COLORS[color].active : ""; + + const tileFile = file(tileNumber); + const tileRank = rank(tileNumber); + const square = algebraic(tileNumber); + + const isFirstColumn = tileFile == (blackPerspective ? FILE.H : FILE.A); + const isLastRow = tileRank == (blackPerspective ? RANK.EIGHTH : RANK.FIRST); + + return ( +
*]:pointer-events-none ${bgColor} ${activeColor}`} + data-tile={tileNumber} + > + {piece == null ? <> : } + +
+ } + > +
+ + + +
+ {square[1]} +
+
+ +
+ {square[0]} +
+
+ + ); +}; + +export default ChessBoard; diff --git a/client/src/utils/useWindowSize.ts b/client/src/utils/useWindowSize.ts new file mode 100644 index 0000000..05117e3 --- /dev/null +++ b/client/src/utils/useWindowSize.ts @@ -0,0 +1,27 @@ +import { useLayoutEffect, useState } from "react"; + +// taken form https://github.com/uidotdev/usehooks +export default function useWindowSize() { + const [size, setSize] = useState({ + width: -1, + height: -1, + }); + + useLayoutEffect(() => { + const handleResize = () => { + setSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + handleResize(); + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + return size; +} diff --git a/client/vite.config.ts b/client/vite.config.ts index 861b04b..830dc01 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,7 +1,16 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + server: { + port: 3000, + proxy: { + "/api": { + target: "http://localhost:5000", + changeOrigin: true, + }, + }, + }, +}); diff --git a/server/package.json b/package.json similarity index 50% rename from server/package.json rename to package.json index a74c764..d85664d 100644 --- a/server/package.json +++ b/package.json @@ -4,23 +4,29 @@ "description": "", "main": "index.js", "scripts": { - "start": "node dist/server.js", - "dev": "nodemon src/server.ts", - "build": "tsc -p .", + "start": "node server/dist/server.js", + "server": "nodemon server/src/server.ts", + "client": "npm run dev --prefix client", + "dev": "concurrently \"npm run server\" \"npm run client\"", + "build:server": "tsc -p .", + "build:client":"npm run build --prefix client", + "build": "concurrently \"npm run build:server\" \"npm run build:client\"", "test": "vitest run" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { + "dotenv": "^16.3.1", "express": "^4.18.2" }, "devDependencies": { "@types/express": "^4.17.17", "@types/node": "^18.15.11", + "concurrently": "^8.2.0", "nodemon": "^2.0.22", "ts-node": "^10.9.1", "typescript": "^5.0.3", "vitest": "^0.29.8" } -} \ No newline at end of file +} diff --git a/server/pnpm-lock.yaml b/pnpm-lock.yaml similarity index 87% rename from server/pnpm-lock.yaml rename to pnpm-lock.yaml index 858e55f..d411bd9 100644 --- a/server/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,13 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: + dotenv: + specifier: ^16.3.1 + version: 16.3.1 express: specifier: ^4.18.2 version: 4.18.2 @@ -12,6 +19,9 @@ devDependencies: '@types/node': specifier: ^18.15.11 version: 18.15.11 + concurrently: + specifier: ^8.2.0 + version: 8.2.0 nodemon: specifier: ^2.0.22 version: 2.0.22 @@ -27,6 +37,13 @@ devDependencies: packages: + /@babel/runtime@7.22.6: + resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -391,6 +408,13 @@ packages: engines: {node: '>=12'} dev: true + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} @@ -494,6 +518,14 @@ packages: type-detect: 4.0.8 dev: true + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + /check-error@1.0.2: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true @@ -521,10 +553,46 @@ packages: string-width: 5.1.2 dev: true + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /concurrently@8.2.0: + resolution: {integrity: sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.1 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + dev: true + /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -550,6 +618,13 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.22.6 + dev: true + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -612,6 +687,11 @@ packages: engines: {node: '>=0.3.1'} dev: true + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -620,6 +700,10 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true @@ -659,6 +743,11 @@ packages: '@esbuild/win32-x64': 0.17.15 dev: true + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} dev: false @@ -750,6 +839,11 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + /get-func-name@2.0.0: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} dev: true @@ -774,6 +868,11 @@ packages: engines: {node: '>=4'} dev: true + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -834,6 +933,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + /is-fullwidth-code-point@4.0.0: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} engines: {node: '>=12'} @@ -860,6 +964,10 @@ packages: engines: {node: '>=14'} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: @@ -1087,6 +1195,15 @@ packages: picomatch: 2.3.1 dev: true + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -1104,6 +1221,12 @@ packages: fsevents: 2.3.2 dev: true + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.0 + dev: true + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: false @@ -1159,6 +1282,10 @@ packages: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: true + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -1196,6 +1323,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + dev: true + /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true @@ -1209,6 +1340,15 @@ packages: resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} dev: true + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + /string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} @@ -1218,6 +1358,13 @@ packages: strip-ansi: 7.0.1 dev: true + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + /strip-ansi@7.0.1: resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} engines: {node: '>=12'} @@ -1238,6 +1385,20 @@ packages: has-flag: 3.0.0 dev: true + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1276,6 +1437,11 @@ packages: nopt: 1.0.10 dev: true + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + /ts-node@10.9.1(@types/node@18.15.11)(typescript@5.0.3): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -1307,6 +1473,10 @@ packages: yn: 3.1.1 dev: true + /tslib@2.6.0: + resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} + dev: true + /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -1481,6 +1651,38 @@ packages: stackback: 0.0.2 dev: true + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} diff --git a/server/src/server.ts b/server/src/server.ts index a87dfe3..0a67082 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,11 +1,36 @@ import express, { Application, Request, Response } from "express"; +import path from "path"; +import dotenv from "dotenv"; -const PORT = process.env.PORT || 3000; +dotenv.config(); + +const PORT = process.env.PORT || 5000; const app: Application = express(); -app.get("/", (req: Request, res: Response) => { - res.send("Hello, world!"); +if (process.env.NODE_ENV === "production") { + const __dirname = path.resolve(); + app.use(express.static(path.join(__dirname, "client", "dist"))); + + app.get("*", (_req, res) => + res.sendFile(path.resolve(__dirname, "client", "dist", "index.html")) + ); +} else app.get("/", (_req, res) => res.send("Hello, world!")); + +app.use((req, res, next) => { + const error = new Error(`Not Found - ${req.originalUrl}`); + res.status(404); + next(error); +}); + +app.use((err: Error, _req: Request, res: Response) => { + let status = res.statusCode == 200 ? 500 : res.statusCode; + let message = err.message; + + res.status(status).json({ + message, + stack: process.env.NODE_ENV !== "production" ? err.stack : null, + }); }); app.listen(PORT, () => diff --git a/server/tsconfig.json b/tsconfig.json similarity index 98% rename from server/tsconfig.json rename to tsconfig.json index 7fa0f23..5e26aa6 100644 --- a/server/tsconfig.json +++ b/tsconfig.json @@ -49,7 +49,7 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "outDir": "./server/dist", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -101,9 +101,9 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "exclude": [ - "src/__test__/" + "server/src/__test__/" ], "include": [ - "src/" + "server/src/" ] }