├── .commitlintrc.json ├── .eslintrc.js ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── README.md ├── apps ├── backend │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── consts.ts │ │ ├── db │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── passport.ts │ │ └── router │ │ │ ├── auth.ts │ │ │ └── v1.ts │ └── tsconfig.json ├── frontend │ ├── .env.example │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── bb.png │ │ ├── bk.png │ │ ├── bn.png │ │ ├── bp.png │ │ ├── bq.png │ │ ├── br.png │ │ ├── capture.wav │ │ ├── chess.png │ │ ├── chessboard.jpeg │ │ ├── computer.png │ │ ├── facebook.png │ │ ├── friendship.png │ │ ├── github.svg │ │ ├── google.svg │ │ ├── lightning-bolt.png │ │ ├── move.wav │ │ ├── strategy.png │ │ ├── theme.svg │ │ ├── trophy.png │ │ ├── vite.svg │ │ ├── wb.png │ │ ├── wk.png │ │ ├── wn.png │ │ ├── wp.png │ │ ├── wq.png │ │ └── wr.png │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── components │ │ │ ├── BackgroundSvg.tsx │ │ │ ├── Button.tsx │ │ │ ├── Card.tsx │ │ │ ├── ChessBoard.tsx │ │ │ ├── ExitGameModel.tsx │ │ │ ├── Footer.tsx │ │ │ ├── GameEndModal.tsx │ │ │ ├── GameModeComponent.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MovesTable.tsx │ │ │ ├── Navbar.tsx │ │ │ ├── PlayerTitle.tsx │ │ │ ├── ShareGame.tsx │ │ │ ├── UserAvatar.tsx │ │ │ ├── chess-board │ │ │ │ ├── ChessSquare.tsx │ │ │ │ ├── LegalMoveIndicator.tsx │ │ │ │ ├── LetterNotation.tsx │ │ │ │ └── NumberNotation.tsx │ │ │ ├── constants │ │ │ │ └── side-nav.tsx │ │ │ ├── mobile-sidebar.tsx │ │ │ ├── side-nav.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── subnav-accordian.tsx │ │ │ ├── themes.tsx │ │ │ └── ui │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ └── waitopponent.tsx │ │ ├── constants │ │ │ └── themes.ts │ │ ├── context │ │ │ └── themeContext.tsx │ │ ├── hooks │ │ │ ├── useSidebar.ts │ │ │ ├── useSocket.ts │ │ │ ├── useThemes.ts │ │ │ └── useWindowSize.ts │ │ ├── index.css │ │ ├── layout │ │ │ └── index.tsx │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── screens │ │ │ ├── Game.tsx │ │ │ ├── Landing.tsx │ │ │ ├── Login.tsx │ │ │ └── Settings.tsx │ │ ├── themes.css │ │ ├── utils │ │ │ └── canvas.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── native │ ├── .gitignore │ ├── app.json │ ├── app │ │ ├── _layout.tsx │ │ ├── game │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── login │ │ │ └── index.tsx │ ├── assets │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ └── splash.png │ ├── babel.config.js │ ├── components │ │ └── chessboard │ │ │ ├── assets │ │ │ ├── bb.png │ │ │ ├── bk.png │ │ │ ├── bn.png │ │ │ ├── bp.png │ │ │ ├── bq.png │ │ │ ├── br.png │ │ │ ├── wb.png │ │ │ ├── wk.png │ │ │ ├── wn.png │ │ │ ├── wp.png │ │ │ ├── wq.png │ │ │ └── wr.png │ │ │ ├── components │ │ │ ├── chessboard-background.tsx │ │ │ ├── highlighted-squares │ │ │ │ ├── highlighted-square.tsx │ │ │ │ └── index.tsx │ │ │ ├── piece │ │ │ │ ├── index.tsx │ │ │ │ └── visual-piece.tsx │ │ │ ├── pieces.tsx │ │ │ └── suggested-dots │ │ │ │ ├── PlaceholderDot.tsx │ │ │ │ └── index.tsx │ │ │ ├── constants.ts │ │ │ ├── context │ │ │ ├── board-context-provider.tsx │ │ │ ├── board-context │ │ │ │ └── index.ts │ │ │ ├── board-operations-context │ │ │ │ ├── hooks.ts │ │ │ │ └── index.tsx │ │ │ ├── board-promotion-context │ │ │ │ ├── dialog │ │ │ │ │ ├── dialog-piece.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── hooks.ts │ │ │ │ └── index.tsx │ │ │ ├── board-refs-context │ │ │ │ ├── contexts.ts │ │ │ │ ├── hooks.ts │ │ │ │ └── index.tsx │ │ │ ├── chess-engine-context │ │ │ │ ├── hooks.ts │ │ │ │ └── index.ts │ │ │ └── props-context │ │ │ │ ├── hooks.ts │ │ │ │ └── index.tsx │ │ │ ├── helpers │ │ │ └── get-chessboard-state.ts │ │ │ ├── hooks │ │ │ └── use-const.ts │ │ │ ├── index.tsx │ │ │ ├── notation.ts │ │ │ └── types.ts │ ├── constants │ │ └── colors.ts │ ├── hooks │ │ └── useSocket.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json └── ws │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── Game.ts │ ├── GameManager.ts │ ├── SocketManager.ts │ ├── auth │ │ └── index.ts │ ├── db │ │ └── index.ts │ ├── index.ts │ └── messages.ts │ └── tsconfig.json ├── package.json ├── packages ├── db │ ├── .env.example │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20240419000456_init │ │ │ │ └── migration.sql │ │ │ ├── 20240419114530_update_schema │ │ │ │ └── migration.sql │ │ │ ├── 20240419114834_add_name │ │ │ │ └── migration.sql │ │ │ ├── 20240419130914_add_from_to │ │ │ │ └── migration.sql │ │ │ ├── 20240420233109_add_guest_auth_provider │ │ │ │ └── migration.sql │ │ │ ├── 20240422001721_add_time_up │ │ │ │ └── migration.sql │ │ │ ├── 20240422115020_add_san_to_move │ │ │ │ └── migration.sql │ │ │ ├── 20240422122937_rename_columns_in_moves_table │ │ │ │ └── migration.sql │ │ │ ├── 20240508152337_remove_facebook_login │ │ │ │ └── migration.sql │ │ │ ├── 20240618172755_add_player_exit │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ └── src │ │ └── index.ts ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── store │ ├── package.json │ └── src │ │ ├── atoms │ │ ├── chessBoard.ts │ │ └── user.ts │ │ └── hooks │ │ └── useUser.ts ├── tailwind-Config │ ├── package.json │ ├── postcss.config.js │ └── tailwind.config.js ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.js │ ├── package.json │ ├── src │ ├── button.tsx │ ├── card.tsx │ └── code.tsx │ ├── tsconfig.json │ ├── tsconfig.lint.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs ├── tsconfig.json ├── turbo.json └── yarn.lock /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-enum": [2, "always", ["ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "assets"]] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // .eslintrc.js 2 | module.exports = { 3 | extends: ["@repo/eslint-config/library.js"], 4 | parser: "@typescript-eslint/parser", 5 | parserOptions: { 6 | project: true, 7 | }, 8 | rules: { 9 | "no-unused-vars": "off", 10 | "no-redeclare": "off", 11 | "turbo/no-undeclared-env-vars": "off", 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/.eslintrc.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged --allow-empty && 5 | npm run lint && npm run format -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.{ts,tsx,json}": ["prettier --write"] 3 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | build 4 | 5 | dist 6 | 7 | .turbo 8 | 9 | 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth":120, 4 | "bracketSpacing": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "semi": true 8 | 9 | } 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Chess 2 | 3 | Building a platform where people can 4 | 5 | 1. Sign up 6 | 2. Create a new match/get connected to an existing match 7 | 3. During the match, let users play moves 8 | 4. Have a rating system that goes up and down similar to standard chess rating 9 | 10 | ## Tech stack 11 | 12 | Let's keep it simple 13 | 14 | 1. React for Frontend 15 | 2. Node.js for Backend 16 | 3. Typescript as the language 17 | 4. Separate Websocket servers for handling real time games 18 | 5. Redis for storing all moves of a game in a queue 19 | 20 | ## Setting it up locally 21 | 22 | - Clone the repo 23 | - Copy over .env.example over to .env everywhere 24 | - Update .env 25 | - Postgres DB Credentials 26 | - Github/Google Auth credentials 27 | - npm install 28 | - Start ws server 29 | - cd apps/ws 30 | - npm run dev 31 | - Start Backend 32 | - cd apps/backend 33 | - npm run dev 34 | - Start frontend 35 | - cd apps/frontend 36 | - npm run dev 37 | 38 | -------------------------------------------------------------------------------- /apps/backend/.env.example: -------------------------------------------------------------------------------- 1 | GOOGLE_CLIENT_ID=your_google_client_id 2 | GOOGLE_CLIENT_SECRET=your_google_client_secret 3 | GITHUB_CLIENT_ID=your_github_client_id 4 | GITHUB_CLIENT_SECRET=your_github_client_secret 5 | 6 | ALLOWED_HOSTS="http://localhost:5173,https://example.com" 7 | 8 | AUTH_REDIRECT_URL="http://localhost:5173/game/random" -------------------------------------------------------------------------------- /apps/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["../../.eslintrc.js"], 3 | parserOptions: { 4 | project: "./tsconfig.json", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/backend/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js", 8 | "start": "node dist/index.js", 9 | "dev": "npm run build && npm run start", 10 | "lint": "eslint . --ext .ts --max-warnings 0", 11 | "lint:fix": "eslint --fix . --ext .ts" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@repo/db": "*", 18 | "@types/express-session": "^1.18.0", 19 | "cookie-parser": "^1.4.6", 20 | "cookie-session": "^2.1.0", 21 | "cors": "^2.8.5", 22 | "dotenv": "^16.4.5", 23 | "express": "^4.19.2", 24 | "express-session": "^1.18.0", 25 | "jsonwebtoken": "^9.0.2", 26 | "passport": "^0.7.0", 27 | "passport-github2": "^0.1.12", 28 | "passport-google-oauth20": "^2.0.0", 29 | "uuid": "^9.0.1" 30 | }, 31 | "devDependencies": { 32 | "@types/cookie-parser": "^1.4.7", 33 | "@types/cookie-session": "^2.0.49", 34 | "@types/cors": "^2.8.17", 35 | "@types/express": "^4.17.21", 36 | "@types/jsonwebtoken": "^9.0.6", 37 | "@types/passport": "^1.0.16" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/backend/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const COOKIE_MAX_AGE = 24 * 60 * 60 * 1000; -------------------------------------------------------------------------------- /apps/backend/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const client = new PrismaClient(); 4 | 5 | export const db = client; 6 | -------------------------------------------------------------------------------- /apps/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import v1Router from './router/v1'; 3 | import cors from 'cors'; 4 | import { initPassport } from './passport'; 5 | import authRoute from './router/auth'; 6 | import dotenv from 'dotenv'; 7 | import session from 'express-session'; 8 | import passport from 'passport'; 9 | import cookieParser from 'cookie-parser'; 10 | import { COOKIE_MAX_AGE } from './consts'; 11 | 12 | const app = express(); 13 | 14 | dotenv.config(); 15 | app.use(express.json()); 16 | app.use(cookieParser()); 17 | app.use( 18 | session({ 19 | secret: process.env.COOKIE_SECRET || 'keyboard cat', 20 | resave: false, 21 | saveUninitialized: false, 22 | cookie: { secure: false, maxAge: COOKIE_MAX_AGE }, 23 | }), 24 | ); 25 | 26 | initPassport(); 27 | app.use(passport.initialize()); 28 | app.use(passport.authenticate('session')); 29 | 30 | const allowedHosts = process.env.ALLOWED_HOSTS 31 | ? process.env.ALLOWED_HOSTS.split(',') 32 | : []; 33 | 34 | app.use( 35 | cors({ 36 | origin: allowedHosts, 37 | methods: 'GET,POST,PUT,DELETE', 38 | credentials: true, 39 | }), 40 | ); 41 | 42 | app.use('/auth', authRoute); 43 | app.use('/v1', v1Router); 44 | 45 | const PORT = process.env.PORT || 3000; 46 | app.listen(PORT, () => { 47 | console.log(`Server is running on port ${PORT}`); 48 | }); 49 | -------------------------------------------------------------------------------- /apps/backend/src/passport.ts: -------------------------------------------------------------------------------- 1 | const GoogleStrategy = require('passport-google-oauth20').Strategy; 2 | const GithubStrategy = require('passport-github2').Strategy; 3 | import passport from 'passport'; 4 | import dotenv from 'dotenv'; 5 | import { db } from './db'; 6 | 7 | interface GithubEmailRes { 8 | email: string; 9 | primary: boolean; 10 | verified: boolean; 11 | visibility: 'private' | 'public'; 12 | } 13 | 14 | dotenv.config(); 15 | const GOOGLE_CLIENT_ID = 16 | process.env.GOOGLE_CLIENT_ID || 'your_google_client_id'; 17 | const GOOGLE_CLIENT_SECRET = 18 | process.env.GOOGLE_CLIENT_SECRET || 'your_google_client_secret'; 19 | const GITHUB_CLIENT_ID = 20 | process.env.GITHUB_CLIENT_ID || 'your_github_client_id'; 21 | const GITHUB_CLIENT_SECRET = 22 | process.env.GITHUB_CLIENT_SECRET || 'your_github_client_secret'; 23 | 24 | export function initPassport() { 25 | if ( 26 | !GOOGLE_CLIENT_ID || 27 | !GOOGLE_CLIENT_SECRET || 28 | !GITHUB_CLIENT_ID || 29 | !GITHUB_CLIENT_SECRET 30 | ) { 31 | throw new Error( 32 | 'Missing environment variables for authentication providers', 33 | ); 34 | } 35 | 36 | passport.use( 37 | new GoogleStrategy( 38 | { 39 | clientID: GOOGLE_CLIENT_ID, 40 | clientSecret: GOOGLE_CLIENT_SECRET, 41 | callbackURL: '/auth/google/callback', 42 | }, 43 | async function ( 44 | accessToken: string, 45 | refreshToken: string, 46 | profile: any, 47 | done: (error: any, user?: any) => void, 48 | ) { 49 | const user = await db.user.upsert({ 50 | create: { 51 | email: profile.emails[0].value, 52 | name: profile.displayName, 53 | provider: 'GOOGLE', 54 | }, 55 | update: { 56 | name: profile.displayName, 57 | }, 58 | where: { 59 | email: profile.emails[0].value, 60 | }, 61 | }); 62 | 63 | done(null, user); 64 | }, 65 | ), 66 | ); 67 | 68 | passport.use( 69 | new GithubStrategy( 70 | { 71 | clientID: GITHUB_CLIENT_ID, 72 | clientSecret: GITHUB_CLIENT_SECRET, 73 | callbackURL: '/auth/github/callback', 74 | }, 75 | async function ( 76 | accessToken: string, 77 | refreshToken: string, 78 | profile: any, 79 | done: (error: any, user?: any) => void, 80 | ) { 81 | const res = await fetch('https://api.github.com/user/emails', { 82 | headers: { 83 | Authorization: `token ${accessToken}`, 84 | }, 85 | }); 86 | const data: GithubEmailRes[] = await res.json(); 87 | const primaryEmail = data.find((item) => item.primary === true); 88 | 89 | const user = await db.user.upsert({ 90 | create: { 91 | email: primaryEmail!.email, 92 | name: profile.displayName, 93 | provider: 'GITHUB', 94 | }, 95 | update: { 96 | name: profile.displayName, 97 | }, 98 | where: { 99 | email: primaryEmail?.email, 100 | }, 101 | }); 102 | 103 | done(null, user); 104 | }, 105 | ), 106 | ); 107 | 108 | passport.serializeUser(function (user: any, cb) { 109 | process.nextTick(function () { 110 | return cb(null, { 111 | id: user.id, 112 | username: user.username, 113 | picture: user.picture, 114 | }); 115 | }); 116 | }); 117 | 118 | passport.deserializeUser(function (user: any, cb) { 119 | process.nextTick(function () { 120 | return cb(null, user); 121 | }); 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /apps/backend/src/router/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import passport from 'passport'; 3 | import jwt from 'jsonwebtoken'; 4 | import { db } from '../db'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | import { COOKIE_MAX_AGE } from '../consts'; 7 | const router = Router(); 8 | 9 | const CLIENT_URL = 10 | process.env.AUTH_REDIRECT_URL ?? 'http://localhost:5173/game/random'; 11 | const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; 12 | 13 | interface userJwtClaims { 14 | userId: string; 15 | name: string; 16 | isGuest?: boolean; 17 | } 18 | 19 | interface UserDetails { 20 | id: string; 21 | token?: string; 22 | name: string; 23 | isGuest?: boolean; 24 | } 25 | 26 | // this route is to be hit when the user wants to login as a guest 27 | router.post('/guest', async (req: Request, res: Response) => { 28 | const bodyData = req.body; 29 | let guestUUID = 'guest-' + uuidv4(); 30 | 31 | const user = await db.user.create({ 32 | data: { 33 | username: guestUUID, 34 | email: guestUUID + '@chess100x.com', 35 | name: bodyData.name || guestUUID, 36 | provider: 'GUEST', 37 | }, 38 | }); 39 | 40 | const token = jwt.sign( 41 | { userId: user.id, name: user.name, isGuest: true }, 42 | JWT_SECRET, 43 | ); 44 | const UserDetails: UserDetails = { 45 | id: user.id, 46 | name: user.name!, 47 | token: token, 48 | isGuest: true, 49 | }; 50 | res.cookie('guest', token, { maxAge: COOKIE_MAX_AGE }); 51 | res.json(UserDetails); 52 | }); 53 | 54 | router.get('/refresh', async (req: Request, res: Response) => { 55 | if (req.user) { 56 | const user = req.user as UserDetails; 57 | 58 | // Token is issued so it can be shared b/w HTTP and ws server 59 | // Todo: Make this temporary and add refresh logic here 60 | 61 | const userDb = await db.user.findFirst({ 62 | where: { 63 | id: user.id, 64 | }, 65 | }); 66 | 67 | const token = jwt.sign({ userId: user.id, name: userDb?.name }, JWT_SECRET); 68 | res.json({ 69 | token, 70 | id: user.id, 71 | name: userDb?.name, 72 | }); 73 | } else if (req.cookies && req.cookies.guest) { 74 | const decoded = jwt.verify(req.cookies.guest, JWT_SECRET) as userJwtClaims; 75 | const token = jwt.sign( 76 | { userId: decoded.userId, name: decoded.name, isGuest: true }, 77 | JWT_SECRET, 78 | ); 79 | let User: UserDetails = { 80 | id: decoded.userId, 81 | name: decoded.name, 82 | token: token, 83 | isGuest: true, 84 | }; 85 | res.cookie('guest', token, { maxAge: COOKIE_MAX_AGE }); 86 | res.json(User); 87 | } else { 88 | res.status(401).json({ success: false, message: 'Unauthorized' }); 89 | } 90 | }); 91 | 92 | router.get('/login/failed', (req: Request, res: Response) => { 93 | res.status(401).json({ success: false, message: 'failure' }); 94 | }); 95 | 96 | router.get('/logout', (req: Request, res: Response) => { 97 | res.clearCookie('guest'); 98 | req.logout((err) => { 99 | if (err) { 100 | console.error('Error logging out:', err); 101 | res.status(500).json({ error: 'Failed to log out' }); 102 | } else { 103 | res.clearCookie('jwt'); 104 | res.redirect('http://localhost:5173/'); 105 | } 106 | }); 107 | }); 108 | 109 | router.get( 110 | '/google', 111 | passport.authenticate('google', { scope: ['profile', 'email'] }), 112 | ); 113 | 114 | router.get( 115 | '/google/callback', 116 | passport.authenticate('google', { 117 | successRedirect: CLIENT_URL, 118 | failureRedirect: '/login/failed', 119 | }), 120 | ); 121 | 122 | router.get( 123 | '/github', 124 | passport.authenticate('github', { scope: ['read:user', 'user:email'] }), 125 | ); 126 | 127 | router.get( 128 | '/github/callback', 129 | passport.authenticate('github', { 130 | successRedirect: CLIENT_URL, 131 | failureRedirect: '/login/failed', 132 | }), 133 | ); 134 | 135 | export default router; 136 | -------------------------------------------------------------------------------- /apps/backend/src/router/v1.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | const v1Router = Router(); 4 | 5 | v1Router.get('/', (req, res) => { 6 | res.send('Hello, World!'); 7 | }); 8 | 9 | export default v1Router; 10 | -------------------------------------------------------------------------------- /apps/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | 12 | }, 13 | "include": [ 14 | "src/**/*", 15 | "routes/**/*", 16 | "**/*.js" 17 | ], 18 | "outDir": "dist" 19 | } 20 | -------------------------------------------------------------------------------- /apps/frontend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_APP_WS_URL="ws://localhost:8080" 2 | VITE_APP_BACKEND_URL="http://localhost:3000" -------------------------------------------------------------------------------- /apps/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /apps/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | .env 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /apps/frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | }; 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /apps/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /apps/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | Chess - Play Chess Online for Free 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-accordion": "^1.1.2", 14 | "@radix-ui/react-alert-dialog": "^1.0.5", 15 | "@radix-ui/react-dialog": "^1.0.5", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-slot": "^1.0.2", 18 | "@repo/store": "*", 19 | "@repo/ui": "*", 20 | "chess.js": "^1.0.0-beta.8", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.0", 23 | "history": "^5.3.0", 24 | "lucide-react": "^0.372.0", 25 | "react": "^18.2.0", 26 | "react-confetti": "^6.1.0", 27 | "react-dom": "^18.2.0", 28 | "react-router-dom": "^6.22.3", 29 | "recoil": "^0.7.7", 30 | "tailwind-merge": "^2.3.0", 31 | "tailwindcss-animate": "^1.0.7", 32 | "zustand": "^4.5.2" 33 | }, 34 | "devDependencies": { 35 | "@types/react": "^18.2.66", 36 | "@types/react-dom": "^18.2.22", 37 | "@typescript-eslint/eslint-plugin": "^7.2.0", 38 | "@typescript-eslint/parser": "^7.2.0", 39 | "@vitejs/plugin-react": "^4.2.1", 40 | "autoprefixer": "^10.4.19", 41 | "eslint": "^8.57.0", 42 | "eslint-plugin-react-hooks": "^4.6.0", 43 | "eslint-plugin-react-refresh": "^0.4.6", 44 | "postcss": "^8.4.38", 45 | "tailwindcss": "^3.4.3", 46 | "typescript": "^5.2.2", 47 | "vite": "^5.2.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/frontend/public/bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/bb.png -------------------------------------------------------------------------------- /apps/frontend/public/bk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/bk.png -------------------------------------------------------------------------------- /apps/frontend/public/bn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/bn.png -------------------------------------------------------------------------------- /apps/frontend/public/bp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/bp.png -------------------------------------------------------------------------------- /apps/frontend/public/bq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/bq.png -------------------------------------------------------------------------------- /apps/frontend/public/br.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/br.png -------------------------------------------------------------------------------- /apps/frontend/public/capture.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/capture.wav -------------------------------------------------------------------------------- /apps/frontend/public/chess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/chess.png -------------------------------------------------------------------------------- /apps/frontend/public/chessboard.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/chessboard.jpeg -------------------------------------------------------------------------------- /apps/frontend/public/computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/computer.png -------------------------------------------------------------------------------- /apps/frontend/public/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/facebook.png -------------------------------------------------------------------------------- /apps/frontend/public/friendship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/friendship.png -------------------------------------------------------------------------------- /apps/frontend/public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/public/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/public/lightning-bolt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/lightning-bolt.png -------------------------------------------------------------------------------- /apps/frontend/public/move.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/move.wav -------------------------------------------------------------------------------- /apps/frontend/public/strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/strategy.png -------------------------------------------------------------------------------- /apps/frontend/public/trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/trophy.png -------------------------------------------------------------------------------- /apps/frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/public/wb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/wb.png -------------------------------------------------------------------------------- /apps/frontend/public/wk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/wk.png -------------------------------------------------------------------------------- /apps/frontend/public/wn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/wn.png -------------------------------------------------------------------------------- /apps/frontend/public/wp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/wp.png -------------------------------------------------------------------------------- /apps/frontend/public/wq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/wq.png -------------------------------------------------------------------------------- /apps/frontend/public/wr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/frontend/public/wr.png -------------------------------------------------------------------------------- /apps/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | width: 100%; 3 | height: 100vh; 4 | } 5 | 6 | .loader__main { 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | gap: 50px; 13 | } 14 | 15 | .loader__dot { 16 | width: 20px; 17 | height: 20px; 18 | background-color: white; 19 | border-radius: 20px; 20 | transform: translate(0px, -40px); 21 | animation: bounce 1s infinite; 22 | } 23 | 24 | .loader__dot:nth-child(2) { 25 | animation-delay: 0.2s; 26 | } 27 | 28 | .loader__dot:nth-child(3) { 29 | animation-delay: 0.4s; 30 | } 31 | 32 | @keyframes bounce { 33 | 0% { 34 | transform: translate(0px, -10px); 35 | } 36 | 40% { 37 | width: 0px; 38 | height: 2px; 39 | transform: translate(0px, 40px) scale(1.7); 40 | } 41 | 100% { 42 | height: 20px; 43 | transform: translate(0px, -20px); 44 | } 45 | } -------------------------------------------------------------------------------- /apps/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import "./App.css"; 5 | import "./themes.css"; 6 | 7 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 8 | import { Landing } from './screens/Landing'; 9 | import { Game } from './screens/Game'; 10 | import Login from './screens/Login'; 11 | import { Suspense } from 'react'; 12 | import { RecoilRoot } from 'recoil'; 13 | import { Loader } from './components/Loader'; 14 | import { Layout } from './layout'; 15 | import { Settings } from './screens/Settings'; 16 | import { Themes } from "./components/themes"; 17 | import { ThemesProvider } from "./context/themeContext"; 18 | 19 | function App() { 20 | return ( 21 |
22 | 23 | }> 24 | 25 | 26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | 33 | function AuthApp() { 34 | return ( 35 | 36 | 37 | } 40 | /> 41 | } 44 | /> 45 | } 48 | /> 49 | } 52 | > 53 | } /> 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /apps/frontend/src/components/BackgroundSvg.tsx: -------------------------------------------------------------------------------- 1 | export default function BackgroundSvg() { 2 | return ( 3 | 9 | 15 | 23 | 31 | 39 | 45 | 51 | 57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 | 105 | 111 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | export const Button = ({ 2 | onClick, 3 | children, 4 | className, 5 | }: { 6 | onClick: () => void; 7 | children: React.ReactNode; 8 | className?: string; 9 | }) => { 10 | return ( 11 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { 3 | Card, 4 | CardContent, 5 | CardDescription, 6 | CardHeader, 7 | CardTitle, 8 | } from '@/components/ui/card'; 9 | import chessIcon from '../../public/chess.png'; 10 | import computerIcon from '../../public/computer.png'; 11 | import lightningIcon from '../../public/lightning-bolt.png'; 12 | import friendIcon from '../../public/friendship.png'; 13 | import tournamentIcon from '../../public/trophy.png'; 14 | import variantsIcon from '../../public/strategy.png'; 15 | import GameModeComponent from './GameModeComponent'; 16 | 17 | export function PlayCard() { 18 | const gameModeData = [ 19 | { 20 | icon: ( 21 | online 26 | ), 27 | title: 'Play Online', 28 | description: 'Play vs a Person of Similar Skill', 29 | onClick: () => { 30 | navigate('/game/random'); 31 | }, 32 | disabled: false, 33 | }, 34 | { 35 | icon: ( 36 | computer 41 | ), 42 | title: 'Computer', 43 | description: 'Challenge a bot from easy to master', 44 | disabled: true, 45 | }, 46 | { 47 | icon: ( 48 | friend 53 | ), 54 | title: 'Play a Friend', 55 | description: 'Invite a Friend to a game of Chess', 56 | disabled: true, 57 | }, 58 | { 59 | icon: ( 60 | tournament 65 | ), 66 | title: 'Tournaments', 67 | description: 'Join an Arena where anyone can Win', 68 | disabled: true, 69 | }, 70 | { 71 | icon: ( 72 | variants 77 | ), 78 | title: 'Chess Variants', 79 | description: 'Find Fun New ways to play chess', 80 | disabled: true, 81 | }, 82 | ]; 83 | 84 | const navigate = useNavigate(); 85 | return ( 86 | 87 | 88 | 89 |

90 | Play Chess 91 |

92 | chess 93 |
94 | 95 |
96 | 97 | {gameModeData.map((data) => { 98 | return ; 99 | })} 100 | 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ExitGameModel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertDialogAction, 3 | AlertDialogCancel, 4 | AlertDialogTitle, 5 | AlertDialogHeader, 6 | AlertDialogFooter, 7 | AlertDialog, 8 | AlertDialogTrigger, 9 | AlertDialogContent, 10 | AlertDialogDescription, 11 | } from './ui/alert-dialog'; 12 | 13 | const ExitGameModel = ({ onClick } : {onClick : () => void}) => { 14 | 15 | return ( 16 | 17 | Exit 18 | 19 | 20 | Are you absolutely sure? 21 | 22 | This action cannot be undone. This will be considered as a loss. 23 | 24 | 25 | 26 | Continue 27 | 31 | Exit 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default ExitGameModel; 40 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | GitHubLogoIcon, 3 | VideoIcon, 4 | TwitterLogoIcon, 5 | } from '@radix-ui/react-icons'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | export const Footer = () => { 9 | return ( 10 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/frontend/src/components/GameEndModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import WhiteKing from '../../public/wk.png'; 3 | import BlackKing from '../../public/bk.png'; 4 | import { GameResult, Result } from '@/screens/Game'; 5 | 6 | interface ModalProps { 7 | blackPlayer?: { id: string; name: string }; 8 | whitePlayer?: { id: string; name: string }; 9 | gameResult: GameResult; 10 | } 11 | 12 | const GameEndModal: React.FC = ({ 13 | blackPlayer, 14 | whitePlayer, 15 | gameResult, 16 | }) => { 17 | const [isOpen, setIsOpen] = useState(true); 18 | 19 | const closeModal = () => { 20 | setIsOpen(false); 21 | }; 22 | 23 | const PlayerDisplay = ({ 24 | player, 25 | gameResult, 26 | isWhite, 27 | }: { 28 | player?: { id: string; name: string }; 29 | gameResult: Result; 30 | isWhite: boolean; 31 | }) => { 32 | const imageSrc = isWhite ? WhiteKing : BlackKing; 33 | const borderColor = 34 | gameResult === (isWhite ? Result.WHITE_WINS : Result.BLACK_WINS) 35 | ? 'border-green-400' 36 | : 'border-red-400'; 37 | 38 | return ( 39 |
40 |
41 | {`${isWhite 46 |
47 |
48 |

49 | {getPlayerName(player)} 50 |

51 |
52 |
53 | ); 54 | }; 55 | 56 | const getWinnerMessage = (result: Result) => { 57 | switch (result) { 58 | case Result.BLACK_WINS: 59 | return 'Black Wins!'; 60 | case Result.WHITE_WINS: 61 | return 'White Wins!'; 62 | default: 63 | return "It's a Draw"; 64 | } 65 | }; 66 | 67 | const getPlayerName = (player: { id: string; name: string } | undefined) => { 68 | return player ? player.name : 'Unknown'; 69 | }; 70 | 71 | return ( 72 |
73 | {isOpen && ( 74 |
75 |
76 |
77 |
78 |
79 |

80 | {getWinnerMessage(gameResult.result)} 81 |

82 |
83 |
84 |

by {gameResult.by}

85 |
86 |
87 | 88 |
vs
89 | 90 |
91 |
92 |
93 | 99 |
100 |
101 |
102 | )} 103 |
104 | ); 105 | }; 106 | 107 | export default GameEndModal; 108 | -------------------------------------------------------------------------------- /apps/frontend/src/components/GameModeComponent.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, MouseEventHandler } from 'react'; 2 | 3 | interface GameModeComponent { 4 | icon: ReactNode; 5 | title: string; 6 | description: string; 7 | onClick?: MouseEventHandler; 8 | disabled: boolean; 9 | } 10 | 11 | const GameModeComponent = ({ 12 | icon, 13 | title, 14 | description, 15 | onClick, 16 | disabled, 17 | }: GameModeComponent) => ( 18 |
22 | {icon} 23 | 24 |
25 |

26 | {title} 27 |

28 |

{description}

29 | {disabled && ( 30 |

Coming Soon ...

31 | )} 32 |
33 |
34 | ); 35 | 36 | export default GameModeComponent; 37 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | export const Loader = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ) 11 | }; 12 | -------------------------------------------------------------------------------- /apps/frontend/src/components/MovesTable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isBoardFlippedAtom, 3 | movesAtom, 4 | userSelectedMoveIndexAtom, 5 | } from '@repo/store/chessBoard'; 6 | import { Move } from 'chess.js'; 7 | import { useEffect, useRef } from 'react'; 8 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 9 | import { 10 | HandshakeIcon, 11 | FlagIcon, 12 | ChevronFirst, 13 | ChevronLast, 14 | ChevronLeft, 15 | ChevronRight, 16 | RefreshCw, 17 | } from 'lucide-react'; 18 | 19 | const MovesTable = () => { 20 | const [userSelectedMoveIndex, setUserSelectedMoveIndex] = useRecoilState( 21 | userSelectedMoveIndexAtom, 22 | ); 23 | const setIsFlipped = useSetRecoilState(isBoardFlippedAtom); 24 | const moves = useRecoilValue(movesAtom); 25 | const movesTableRef = useRef(null); 26 | const movesArray = moves.reduce((result, _, index: number, array: Move[]) => { 27 | if (index % 2 === 0) { 28 | result.push(array.slice(index, index + 2)); 29 | } 30 | return result; 31 | }, [] as Move[][]); 32 | 33 | useEffect(() => { 34 | if (movesTableRef && movesTableRef.current) { 35 | movesTableRef.current.scrollTo({ 36 | top: movesTableRef.current.scrollHeight, 37 | behavior: 'smooth', 38 | }); 39 | } 40 | }, [moves]); 41 | return ( 42 |
43 |
47 | {movesArray.map((movePairs, index) => { 48 | return ( 49 |
53 |
54 | {`${index + 1}.`} 55 | 56 | {movePairs.map((move, movePairIndex) => { 57 | const isLastIndex = 58 | movePairIndex === movePairs.length - 1 && 59 | movesArray.length - 1 === index; 60 | const isHighlighted = 61 | userSelectedMoveIndex !== null 62 | ? userSelectedMoveIndex === index * 2 + movePairIndex 63 | : isLastIndex; 64 | const { san } = move; 65 | 66 | return ( 67 |
{ 71 | setUserSelectedMoveIndex(index * 2 + movePairIndex); 72 | }} 73 | > 74 | {san} 75 |
76 | ); 77 | })} 78 |
79 |
80 | ); 81 | })} 82 |
83 | {moves.length ? ( 84 |
85 |
86 | 90 | 94 |
95 |
96 | 106 | 107 | 118 | 133 | 143 | 151 |
152 |
153 | ) : null} 154 |
155 | ); 156 | }; 157 | 158 | export default MovesTable; 159 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { MobileSidebar } from '@/components/mobile-sidebar'; 2 | import { Button } from './ui/button'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | export default function Navbar() { 6 | const navigate = useNavigate(); 7 | return ( 8 |
9 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/frontend/src/components/PlayerTitle.tsx: -------------------------------------------------------------------------------- 1 | import { Player } from "../screens/Game" 2 | 3 | interface PlayerTitleProps { 4 | player: Player | undefined 5 | isSelf?: boolean 6 | } 7 | 8 | export const PlayerTitle = ({player, isSelf}: PlayerTitleProps) => { 9 | return ( 10 |
11 | {player && player.id.startsWith("guest") && 12 |

13 | [Guest] 14 |

15 | } 16 |

{player && player.name}

17 | {isSelf && 18 |

(You)

19 | } 20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /apps/frontend/src/components/ShareGame.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { Button } from "./Button"; 3 | 4 | export const ShareGame = ({className,gameId}:{className?:string,gameId:string}) => { 5 | 6 | const url = window.origin+"/game/"+gameId; 7 | const [copied,setCopied] = useState(false); 8 | 9 | const handleCopy = ()=>{ 10 | window.navigator.clipboard.writeText(url); 11 | setCopied((p)=>true); 12 | } 13 | 14 | return ( 15 |
16 | 17 |

18 | Play with Friends 19 |

20 | 21 |
22 | 23 | 24 | 25 |
26 | {url} 27 |
28 |
29 | 30 | 33 |
34 | ) 35 | } 36 | 37 | const LinkSvg = () => { 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } -------------------------------------------------------------------------------- /apps/frontend/src/components/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { useUser } from '@repo/store/useUser'; 2 | import { Metadata, Player } from '../screens/Game'; 3 | 4 | interface UserAvatarProps { 5 | gameMetadata: Metadata | null; 6 | self?: boolean; 7 | } 8 | 9 | export const UserAvatar = ({ gameMetadata, self }: UserAvatarProps) => { 10 | const user = useUser(); 11 | let player: Player; 12 | if (gameMetadata?.blackPlayer.id === user.id) { 13 | player = self ? gameMetadata.blackPlayer : gameMetadata.whitePlayer; 14 | } else { 15 | player = self ? gameMetadata?.whitePlayer! : gameMetadata?.blackPlayer!; 16 | } 17 | 18 | return ( 19 |
20 |

{player?.name}

21 | {player?.isGuest &&

[Guest]

} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /apps/frontend/src/components/chess-board/ChessSquare.tsx: -------------------------------------------------------------------------------- 1 | import { Color, PieceSymbol, Square } from 'chess.js'; 2 | 3 | const ChessSquare = ({ 4 | square, 5 | }: { 6 | square: { 7 | square: Square; 8 | type: PieceSymbol; 9 | color: Color; 10 | }; 11 | }) => { 12 | return ( 13 |
14 | {square ? ( 15 | 19 | ) : null} 20 |
21 | ); 22 | }; 23 | 24 | export default ChessSquare; -------------------------------------------------------------------------------- /apps/frontend/src/components/chess-board/LegalMoveIndicator.tsx: -------------------------------------------------------------------------------- 1 | const LegalMoveIndicator = ({ 2 | isPiece, 3 | isMainBoxColor, 4 | }: { 5 | isPiece: boolean; 6 | isMainBoxColor: boolean; 7 | }) => { 8 | return ( 9 |
10 | {isPiece ? ( 11 |
14 | ) : ( 15 |
18 | )} 19 |
20 | ); 21 | }; 22 | 23 | export default LegalMoveIndicator; 24 | -------------------------------------------------------------------------------- /apps/frontend/src/components/chess-board/LetterNotation.tsx: -------------------------------------------------------------------------------- 1 | const LetterNotation = ({ 2 | label, 3 | isMainBoxColor, 4 | }: { 5 | label: string; 6 | isMainBoxColor: boolean; 7 | }) => { 8 | return ( 9 |
12 | {label} 13 |
14 | ); 15 | }; 16 | 17 | export default LetterNotation; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/components/chess-board/NumberNotation.tsx: -------------------------------------------------------------------------------- 1 | const NumberNotation = ({ 2 | label, 3 | isMainBoxColor, 4 | }: { 5 | label: string; 6 | isMainBoxColor: boolean; 7 | }) => { 8 | return ( 9 |
12 | {label} 13 |
14 | ); 15 | }; 16 | 17 | export default NumberNotation; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/components/constants/side-nav.tsx: -------------------------------------------------------------------------------- 1 | import { PuzzleIcon, LogInIcon, LogOutIcon, SettingsIcon } from 'lucide-react'; 2 | const BACKEND_URL = 3 | import.meta.env.VITE_APP_BACKEND_URL ?? 'http://localhost:3000'; 4 | export const UpperNavItems = [ 5 | { 6 | title: 'Play', 7 | icon: PuzzleIcon, 8 | href: '/game/random', 9 | color: 'text-green-500', 10 | }, 11 | // 12 | // { 13 | // title: 'Puzzles', 14 | // icon: PuzzleIcon, 15 | // href: '/', 16 | // color: 'text-sky-500', 17 | // }, 18 | // { 19 | // title: 'Learn', 20 | // icon: PuzzleIcon, 21 | // href: '/', 22 | // color: 'text-sky-500', 23 | // }, 24 | ]; 25 | 26 | export const LowerNavItems = [ 27 | { 28 | title: 'Login', 29 | icon: LogInIcon, 30 | href: '/login', 31 | color: 'text-green-500', 32 | }, 33 | { 34 | title: 'Logout', 35 | icon: LogOutIcon, 36 | href: `${BACKEND_URL}/auth/logout`, 37 | color: 'text-green-500', 38 | }, 39 | { 40 | title: 'Settings', 41 | icon: SettingsIcon, 42 | href: '/settings', 43 | color: 'text-green-500', 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /apps/frontend/src/components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { MenuIcon } from 'lucide-react'; 3 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; 4 | import { SideNav } from '@/components/side-nav'; 5 | import { UpperNavItems, LowerNavItems } from '@/components/constants/side-nav'; 6 | 7 | export const MobileSidebar = () => { 8 | const [open, setOpen] = useState(false); 9 | const [isMounted, setIsMounted] = useState(false); 10 | 11 | useEffect(() => { 12 | setIsMounted(true); 13 | }, []); 14 | 15 | if (!isMounted) { 16 | return null; 17 | } 18 | 19 | return ( 20 | <> 21 | 22 | 23 |
24 | 25 |

100xchess

26 |
27 |
28 | 32 |
33 |

34 | 100xchess 35 |

36 | 37 |
38 |
39 | 40 |
41 |
42 |
43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /apps/frontend/src/components/side-nav.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { useSidebar } from '@/hooks/useSidebar'; 3 | import { buttonVariants } from '@/components/ui/button'; 4 | import { useLocation } from 'react-router-dom'; 5 | import { 6 | Accordion, 7 | AccordionContent, 8 | AccordionItem, 9 | AccordionTrigger, 10 | } from '@/components/subnav-accordian'; 11 | import { useEffect, useState } from 'react'; 12 | import { ChevronDownIcon } from '@radix-ui/react-icons'; 13 | import { type LucideIcon } from 'lucide-react'; 14 | import { useUser } from '@repo/store/useUser'; 15 | 16 | export interface NavItem { 17 | title: string; 18 | href: string; 19 | icon: LucideIcon; 20 | color?: string; 21 | isChidren?: boolean; 22 | children?: NavItem[]; 23 | } 24 | 25 | interface SideNavProps { 26 | items: NavItem[]; 27 | setOpen?: (open: boolean) => void; 28 | className?: string; 29 | } 30 | 31 | export function SideNav({ items, setOpen, className }: SideNavProps) { 32 | const user = useUser(); 33 | const location = useLocation(); 34 | const { isOpen } = useSidebar(); 35 | const [openItem, setOpenItem] = useState(''); 36 | const [lastOpenItem, setLastOpenItem] = useState(''); 37 | 38 | useEffect(() => { 39 | if (isOpen) { 40 | setOpenItem(lastOpenItem); 41 | } else { 42 | setLastOpenItem(openItem); 43 | setOpenItem(''); 44 | } 45 | }, [isOpen]); 46 | 47 | return ( 48 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /apps/frontend/src/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { SideNav } from '@/components/side-nav'; 3 | import { UpperNavItems, LowerNavItems } from '@/components/constants/side-nav'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | import { useSidebar } from '@/hooks/useSidebar'; 7 | interface SidebarProps { 8 | className?: string; 9 | } 10 | 11 | export default function Sidebar({ className }: SidebarProps) { 12 | const { isOpen, toggle } = useSidebar(); 13 | useEffect(() => { 14 | const handleResize = () => { 15 | const screenWidth = window.innerWidth; 16 | const isBetweenMDAndLG = screenWidth >= 768 && screenWidth < 1024; 17 | if (isBetweenMDAndLG) { 18 | if (isOpen) { 19 | toggle(); 20 | } 21 | } else { 22 | if (!isOpen) { 23 | toggle(); 24 | } 25 | } 26 | }; 27 | 28 | handleResize(); 29 | window.addEventListener('resize', handleResize); 30 | 31 | return () => { 32 | window.removeEventListener('resize', handleResize); 33 | }; 34 | }, [isOpen, toggle]); 35 | return ( 36 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /apps/frontend/src/components/subnav-accordian.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const Accordion = AccordionPrimitive.Root; 7 | 8 | const AccordionItem = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 17 | )); 18 | AccordionItem.displayName = 'AccordionItem'; 19 | 20 | const AccordionTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, children, ...props }, ref) => ( 24 | 25 | svg]:rotate-180', 29 | className, 30 | )} 31 | {...props} 32 | > 33 | {children} 34 | 35 | 36 | )); 37 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 38 | 39 | const AccordionContent = React.forwardRef< 40 | React.ElementRef, 41 | React.ComponentPropsWithoutRef 42 | >(({ className, children, ...props }, ref) => ( 43 | 48 |
{children}
49 |
50 | )); 51 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 52 | 53 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 54 | -------------------------------------------------------------------------------- /apps/frontend/src/components/themes.tsx: -------------------------------------------------------------------------------- 1 | import { THEMES_DATA } from "@/constants/themes"; 2 | import { useThemeContext } from "@/hooks/useThemes"; 3 | 4 | export function Themes() { 5 | const { updateTheme } = useThemeContext(); 6 | return ( 7 |
8 |
9 | { 10 | THEMES_DATA.map(theme => { 11 | return ( 12 |
{ 17 | updateTheme(theme.name); 18 | }} 19 | > 20 |
21 |

{theme.name}

22 |
23 |
24 | chess-piece 30 | chess-piece 36 | chess-piece 42 | chess-piece 48 |
49 |
50 | ) 51 | }) 52 | } 53 |
54 |
55 | ) 56 | } -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | import { buttonVariants } from '@/components/ui/button'; 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root; 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 10 | 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal; 12 | 13 | const AlertDialogOverlay = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, ...props }, ref) => ( 17 | 25 | )); 26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 27 | 28 | const AlertDialogContent = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 33 | 34 | 42 | 43 | )); 44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 45 | 46 | const AlertDialogHeader = ({ 47 | className, 48 | ...props 49 | }: React.HTMLAttributes) => ( 50 |
57 | ); 58 | AlertDialogHeader.displayName = 'AlertDialogHeader'; 59 | 60 | const AlertDialogFooter = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ); 72 | AlertDialogFooter.displayName = 'AlertDialogFooter'; 73 | 74 | const AlertDialogTitle = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef 77 | >(({ className, ...props }, ref) => ( 78 | 83 | )); 84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 85 | 86 | const AlertDialogDescription = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 95 | )); 96 | AlertDialogDescription.displayName = 97 | AlertDialogPrimitive.Description.displayName; 98 | 99 | const AlertDialogAction = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 110 | 111 | const AlertDialogCancel = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 124 | )); 125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 126 | 127 | export { 128 | AlertDialog, 129 | AlertDialogPortal, 130 | AlertDialogOverlay, 131 | AlertDialogTrigger, 132 | AlertDialogContent, 133 | AlertDialogHeader, 134 | AlertDialogFooter, 135 | AlertDialogTitle, 136 | AlertDialogDescription, 137 | AlertDialogAction, 138 | AlertDialogCancel, 139 | }; 140 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as SheetPrimitive from '@radix-ui/react-dialog'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | import { X } from 'lucide-react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const Sheet = SheetPrimitive.Root; 9 | 10 | const SheetTrigger = SheetPrimitive.Trigger; 11 | 12 | const SheetClose = SheetPrimitive.Close; 13 | 14 | const SheetPortal = SheetPrimitive.Portal; 15 | 16 | const SheetOverlay = React.forwardRef< 17 | React.ElementRef, 18 | React.ComponentPropsWithoutRef 19 | >(({ className, ...props }, ref) => ( 20 | 28 | )); 29 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; 30 | 31 | const sheetVariants = cva( 32 | 'fixed z-50 gap-4 bg-stone-800 pt-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', 33 | { 34 | variants: { 35 | side: { 36 | top: 'inset-x-0 top-0 data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', 37 | bottom: 38 | 'inset-x-0 bottom-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', 39 | left: 'inset-y-0 left-0 h-full w-3/4 data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', 40 | right: 41 | 'inset-y-0 right-0 h-full w-3/4 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', 42 | }, 43 | }, 44 | defaultVariants: { 45 | side: 'right', 46 | }, 47 | }, 48 | ); 49 | 50 | interface SheetContentProps 51 | extends React.ComponentPropsWithoutRef, 52 | VariantProps {} 53 | 54 | const SheetContent = React.forwardRef< 55 | React.ElementRef, 56 | SheetContentProps 57 | >(({ side = 'right', className, children, ...props }, ref) => ( 58 | 59 | 60 | 65 | {children} 66 | 67 | 68 | Close 69 | 70 | 71 | 72 | )); 73 | SheetContent.displayName = SheetPrimitive.Content.displayName; 74 | 75 | const SheetHeader = ({ 76 | className, 77 | ...props 78 | }: React.HTMLAttributes) => ( 79 |
86 | ); 87 | SheetHeader.displayName = 'SheetHeader'; 88 | 89 | const SheetFooter = ({ 90 | className, 91 | ...props 92 | }: React.HTMLAttributes) => ( 93 |
100 | ); 101 | SheetFooter.displayName = 'SheetFooter'; 102 | 103 | const SheetTitle = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )); 113 | SheetTitle.displayName = SheetPrimitive.Title.displayName; 114 | 115 | const SheetDescription = React.forwardRef< 116 | React.ElementRef, 117 | React.ComponentPropsWithoutRef 118 | >(({ className, ...props }, ref) => ( 119 | 124 | )); 125 | SheetDescription.displayName = SheetPrimitive.Description.displayName; 126 | 127 | export { 128 | Sheet, 129 | SheetPortal, 130 | SheetOverlay, 131 | SheetTrigger, 132 | SheetClose, 133 | SheetContent, 134 | SheetHeader, 135 | SheetFooter, 136 | SheetTitle, 137 | SheetDescription, 138 | }; 139 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/waitopponent.tsx: -------------------------------------------------------------------------------- 1 | export function Waitopponent() { 2 | return ( 3 |
4 |
5 | Wait opponent will join soon... 6 |
7 |
11 | 27 | Loading... 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/frontend/src/constants/themes.ts: -------------------------------------------------------------------------------- 1 | import { THEME } from "@/context/themeContext" 2 | 3 | type THEME_DATA = { 4 | id: number, 5 | name: THEME, 6 | background: string, 7 | "board-light": string, 8 | "board-dark": string, 9 | "board-image": string 10 | } 11 | 12 | export const THEMES_DATA: THEME_DATA[] = [ 13 | { 14 | id: 1, 15 | name: "default", 16 | background: "#302E2B", 17 | "board-light": "#EBECD0", 18 | "board-dark": "#739552", 19 | "board-image": "https://www.chess.com/bundles/web/images/offline-play/standardboard.1d6f9426.png", 20 | }, 21 | { 22 | id: 2, 23 | name: "bubblegum", 24 | background: "#E6455E", 25 | "board-light": "#FEFFFE", 26 | "board-dark": "#FBD9E1", 27 | "board-image": "https://res.cloudinary.com/dcugqfvvg/image/upload/e_improve,e_sharpen/v1718047051/screenshot-localhost_5173-2024.06.11-00_44_01_pxwr43.png", 28 | } 29 | ] -------------------------------------------------------------------------------- /apps/frontend/src/context/themeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState } from "react"; 2 | 3 | export type THEME = "default" | "bubblegum"; 4 | 5 | export type THEME_CONTEXT = { 6 | theme: THEME, 7 | updateTheme: (theme: THEME) => void 8 | } 9 | 10 | const AVAILABLE_THEMES: THEME[] = ["default", "bubblegum"]; 11 | 12 | export const ThemeContext = createContext(null); 13 | 14 | export function ThemesProvider({ children }: { children: React.ReactNode }) { 15 | const [theme, setTheme] = useState("default"); 16 | 17 | function updateTheme(theme: THEME) { 18 | setTheme(theme); 19 | localStorage.setItem("theme", theme); 20 | document.querySelector("html")?.setAttribute("data-theme", theme); 21 | } 22 | 23 | useEffect(() => { 24 | const currentTheme = localStorage.getItem("theme") as THEME | null; 25 | 26 | if(currentTheme && AVAILABLE_THEMES.includes(currentTheme)) { 27 | setTheme(currentTheme); 28 | document.querySelector("html")?.setAttribute("data-theme", currentTheme); 29 | } 30 | }, []); 31 | 32 | return ( 33 | 37 | {children} 38 | 39 | ) 40 | } 41 | 42 | -------------------------------------------------------------------------------- /apps/frontend/src/hooks/useSidebar.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface SidebarStore { 4 | isOpen: boolean; 5 | toggle: () => void; 6 | } 7 | 8 | export const useSidebar = create((set: any) => ({ 9 | isOpen: true, 10 | toggle: () => set((state: any) => ({ isOpen: !state.isOpen })), 11 | })); 12 | -------------------------------------------------------------------------------- /apps/frontend/src/hooks/useSocket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useUser } from '@repo/store/useUser'; 3 | 4 | const WS_URL = import.meta.env.VITE_APP_WS_URL ?? 'ws://localhost:8080'; 5 | 6 | export const useSocket = () => { 7 | const [socket, setSocket] = useState(null); 8 | const user = useUser(); 9 | 10 | useEffect(() => { 11 | if (!user) return; 12 | const ws = new WebSocket(`${WS_URL}?token=${user.token}`); 13 | 14 | ws.onopen = () => { 15 | setSocket(ws); 16 | }; 17 | 18 | ws.onclose = () => { 19 | setSocket(null); 20 | }; 21 | 22 | return () => { 23 | ws.close(); 24 | }; 25 | }, [user]); 26 | 27 | return socket; 28 | }; 29 | -------------------------------------------------------------------------------- /apps/frontend/src/hooks/useThemes.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { THEME_CONTEXT, ThemeContext } from "@/context/themeContext"; 3 | 4 | export function useThemeContext() { 5 | const data = useContext(ThemeContext) as THEME_CONTEXT; 6 | return data; 7 | } -------------------------------------------------------------------------------- /apps/frontend/src/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from 'react'; 2 | 3 | const useWindowSize = () => { 4 | const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); 5 | 6 | const handleSize = () => { 7 | setWindowSize({ 8 | width: window.innerWidth, 9 | height: window.innerHeight, 10 | }); 11 | }; 12 | 13 | useLayoutEffect(() => { 14 | handleSize(); 15 | 16 | window.addEventListener('resize', handleSize); 17 | 18 | return () => window.removeEventListener('resize', handleSize); 19 | }, []); 20 | 21 | return windowSize; 22 | }; 23 | 24 | export default useWindowSize; 25 | -------------------------------------------------------------------------------- /apps/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 0 0% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 0 0% 3.9%; 15 | 16 | --primary: 0 0% 9%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | 22 | --muted: 0 0% 96.1%; 23 | --muted-foreground: 0 0% 45.1%; 24 | 25 | --accent: 0 0% 96.1%; 26 | --accent-foreground: 0 0% 9%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 0 0% 89.8%; 32 | --input: 0 0% 89.8%; 33 | --ring: 0 0% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 0 0% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 0 0% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 0 0% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 0 0% 9%; 50 | 51 | --secondary: 0 0% 14.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | 57 | --accent: 0 0% 14.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 0 0% 14.9%; 64 | --input: 0 0% 14.9%; 65 | --ring: 0 0% 83.1%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | .chess-board { 79 | background-color: #302e2b; 80 | } 81 | 82 | @layer utilities { 83 | /* Hide scrollbar for Chrome, Safari and Opera */ 84 | .no-scrollbar::-webkit-scrollbar { 85 | display: none; 86 | } 87 | /* Hide scrollbar for IE, Edge and Firefox */ 88 | .no-scrollbar { 89 | -ms-overflow-style: none; /* IE and Edge */ 90 | scrollbar-width: none; /* Firefox */ 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /apps/frontend/src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Sidebar from '@/components/sidebar'; 3 | 4 | export const Layout = ({ children }: { children: React.ReactNode }) => { 5 | return ( 6 |
7 | 8 |
9 | {children} 10 |
11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /apps/frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './App.tsx'; 3 | import './index.css'; 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render(); 6 | -------------------------------------------------------------------------------- /apps/frontend/src/screens/Landing.tsx: -------------------------------------------------------------------------------- 1 | import { PlayCard } from '@/components/Card'; 2 | import { Footer } from '@/components/Footer'; 3 | import { useThemeContext } from '@/hooks/useThemes'; 4 | import { THEMES_DATA } from '@/constants/themes'; 5 | 6 | export const Landing = () => { 7 | const { theme } = useThemeContext(); 8 | const currentTheme = THEMES_DATA.find(data => data.name === theme); 9 | return ( 10 | <> 11 |
12 |
13 | { 14 | currentTheme ? ( 15 | chess-board 20 | ) : ( 21 | chess-board 26 | )} 27 | 28 |
29 |
30 |
31 |
32 |
33 | chess-board 34 |
35 |
36 |

Found an Issue!

37 |

Please create an issue in our github website below. You are also invited to contribute on the project.

38 | 43 | icon 44 |

Github

45 |
46 |
47 |
48 |
49 |