├── .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 |
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 |
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 |
41 | ),
42 | title: 'Computer',
43 | description: 'Challenge a bot from easy to master',
44 | disabled: true,
45 | },
46 | {
47 | icon: (
48 |
53 | ),
54 | title: 'Play a Friend',
55 | description: 'Invite a Friend to a game of Chess',
56 | disabled: true,
57 | },
58 | {
59 | icon: (
60 |
65 | ),
66 | title: 'Tournaments',
67 | description: 'Join an Arena where anyone can Win',
68 | disabled: true,
69 | },
70 | {
71 | icon: (
72 |
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 |
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 |

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 |
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 |
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 |
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 |
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 |

20 | ) : (
21 |

26 | )}
27 |
28 |
29 |
30 |
31 |
32 |
33 |

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 |
44 | Github
45 |
46 |
47 |
48 |
49 |
50 | >
51 | );
52 | };
--------------------------------------------------------------------------------
/apps/frontend/src/screens/Login.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import { useRef } from 'react';
3 | import { useRecoilState } from 'recoil';
4 | import { userAtom } from '@repo/store/userAtom';
5 |
6 | const BACKEND_URL =
7 | import.meta.env.VITE_APP_BACKEND_URL ?? 'http://localhost:3000';
8 |
9 | const Login = () => {
10 | const navigate = useNavigate();
11 | const guestName = useRef(null);
12 | const [_, setUser] = useRecoilState(userAtom);
13 |
14 | const google = () => {
15 | window.open(`${BACKEND_URL}/auth/google`, '_self');
16 | };
17 |
18 | const github = () => {
19 | window.open(`${BACKEND_URL}/auth/github`, '_self');
20 | };
21 |
22 | const loginAsGuest = async () => {
23 | const response = await fetch(`${BACKEND_URL}/auth/guest`, {
24 | method: 'POST',
25 | headers: {
26 | 'Content-Type': 'application/json',
27 | },
28 | credentials: 'include',
29 | body: JSON.stringify({
30 | name: (guestName.current && guestName.current.value) || '',
31 | }),
32 | });
33 | const user = await response.json();
34 | setUser(user);
35 | navigate('/game/random');
36 | };
37 |
38 | return (
39 |
40 |
41 | Enter the Game World
42 |
43 |
44 |
45 |
49 |

50 | Sign in with Google
51 |
52 |
56 |

57 | Sign in with Github
58 |
59 |
60 |
61 |
66 |
72 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default Login;
85 |
--------------------------------------------------------------------------------
/apps/frontend/src/screens/Settings.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Outlet } from "react-router-dom";
2 |
3 | export const Settings = () => {
4 | return (
5 |
6 |
Settings
7 |
8 |
9 |
10 |
11 |

12 |
Themes
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/apps/frontend/src/themes.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg-color-main: #302e2b;
3 | --text-color-main: #fff;
4 | --bg-color-auxiliary-1: rgba(0, 0, 0, 0.2);
5 | --bg-color-auxiliary-2: rgba(0, 0, 0, 0.6);
6 | --bg-color-auxiliary-3: rgba(0, 0, 0, 0.1);
7 | --board-light: #EBECD0;
8 | --board-dark: #739552;
9 | }
10 |
11 | [data-theme="bubblegum"]:root {
12 | --bg-color-main: #E6455E;
13 | --text-color-main: #fff;
14 | --bg-color-auxiliary-1: rgba(0, 0, 0, 0.2);
15 | --bg-color-auxiliary-2: rgba(0, 0, 0, 0.6);
16 | --bg-color-auxiliary-3: rgba(0, 0, 0, 0.1);
17 | --board-light:#FEFFFE;
18 | --board-dark: #FBD9E1;
19 | }
--------------------------------------------------------------------------------
/apps/frontend/src/utils/canvas.ts:
--------------------------------------------------------------------------------
1 |
2 | const calculateX = (square: string, isFlipped: boolean, squareSize: number) => {
3 |
4 | let columnIndex = square.charCodeAt(0) - 'a'.charCodeAt(0); // Convert column letter to index
5 | if (isFlipped) {
6 | columnIndex = 7 - columnIndex; // Reverse the column index if the board is flipped
7 | }
8 |
9 | return columnIndex * squareSize + squareSize / 2;
10 | };
11 |
12 | const calculateY = (square: string, isFlipped: boolean, squareSize: number) => {
13 |
14 | let rowIndex = 8 - parseInt(square[1]); // Convert row number to index (assuming rank 1 is at the bottom)
15 | if (isFlipped) {
16 | rowIndex = 7 - rowIndex; // Reverse the row index if the board is flipped
17 | }
18 | return rowIndex * squareSize + squareSize / 2;
19 | };
20 | export const drawArrow = (
21 | {
22 | ctx, start, end, isFlipped, squareSize
23 | }:{ ctx: CanvasRenderingContext2D,
24 | start: string,
25 | end: string,
26 | isFlipped: boolean,squareSize: number}
27 | ) => {
28 | const startX = calculateX(start, isFlipped, squareSize);
29 | const startY = calculateY(start, isFlipped, squareSize);
30 | const endX = calculateX(end, isFlipped, squareSize);
31 | const endY = calculateY(end, isFlipped, squareSize);
32 |
33 | // Draw arrow
34 | ctx.beginPath();
35 | ctx.moveTo(startX, startY);
36 | ctx.lineTo(endX, endY);
37 | ctx.strokeStyle = '#EC923F';
38 | ctx.lineWidth = 2;
39 | ctx.stroke();
40 |
41 | // Draw arrowhead
42 | const angle = Math.atan2(endY - startY, endX - startX);
43 | const arrowheadSize = 15; // Adjust arrowhead size as needed
44 | const arrowheadX1 = endX - arrowheadSize * Math.cos(angle - Math.PI / 6);
45 | const arrowheadY1 = endY - arrowheadSize * Math.sin(angle - Math.PI / 6);
46 | const arrowheadX2 = endX - arrowheadSize * Math.cos(angle + Math.PI / 6);
47 | const arrowheadY2 = endY - arrowheadSize * Math.sin(angle + Math.PI / 6);
48 |
49 | ctx.beginPath();
50 | ctx.moveTo(endX, endY);
51 | ctx.lineTo(arrowheadX1, arrowheadY1);
52 | ctx.moveTo(endX, endY);
53 | ctx.lineTo(arrowheadX2, arrowheadY2);
54 | ctx.strokeStyle = '#EC923F';
55 | ctx.lineWidth = 2;
56 | ctx.stroke();
57 | };
58 |
--------------------------------------------------------------------------------
/apps/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | prefix: "",
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | bgMain: "var(--bg-color-main)",
55 | bgAuxiliary1: "var(--bg-color-auxiliary-1)",
56 | bgAuxiliary2: "var(--bg-color-auxiliary-2)",
57 | bgAuxiliary3: "var(--bg-color-auxiliary-3)",
58 | textMain: "var(--text-color-main)",
59 | boardDark: "var(--board-dark)",
60 | boardLight: "var(--board-light)"
61 | },
62 | borderRadius: {
63 | lg: "var(--radius)",
64 | md: "calc(var(--radius) - 2px)",
65 | sm: "calc(var(--radius) - 4px)",
66 | },
67 | keyframes: {
68 | "accordion-down": {
69 | from: { height: "0" },
70 | to: { height: "var(--radix-accordion-content-height)" },
71 | },
72 | "accordion-up": {
73 | from: { height: "var(--radix-accordion-content-height)" },
74 | to: { height: "0" },
75 | },
76 | },
77 | animation: {
78 | "accordion-down": "accordion-down 0.2s ease-out",
79 | "accordion-up": "accordion-up 0.2s ease-out",
80 | },
81 | },
82 | },
83 | plugins: [require("tailwindcss-animate")],
84 | }
--------------------------------------------------------------------------------
/apps/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/*": ["./src/*"]
19 | },
20 |
21 | /* Linting */
22 | "strict": true,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 | "noFallthroughCasesInSwitch": true
26 | },
27 | "include": ["src"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/apps/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/apps/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import react from '@vitejs/plugin-react';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | '@': path.resolve(__dirname, './src'),
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/apps/native/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
--------------------------------------------------------------------------------
/apps/native/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "chessnative",
4 | "slug": "chessnative",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "scheme": "chess-native",
10 | "splash": {
11 | "image": "./assets/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "assetBundlePatterns": [
16 | "**/*"
17 | ],
18 | "ios": {
19 | "supportsTablet": true
20 | },
21 | "android": {
22 | "adaptiveIcon": {
23 | "foregroundImage": "./assets/adaptive-icon.png",
24 | "backgroundColor": "#ffffff"
25 | }
26 | },
27 | "web": {
28 | "favicon": "./assets/favicon.png"
29 | },
30 | "plugins": [
31 | "expo-router"
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/native/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Stack, router } from 'expo-router';
2 | import { SafeAreaProvider } from 'react-native-safe-area-context';
3 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
4 | import { useEffect } from 'react';
5 | import { PRIMARY_BROWN } from '../constants/colors';
6 | // import Loading from "../components/loading";
7 |
8 | export default function RootLayoutNav() {
9 | useEffect(() => {
10 | router.replace('/login');
11 | }, []);
12 |
13 | return (
14 |
15 |
16 |
29 |
35 |
41 |
42 | {/* */}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/native/app/game/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useRef, useState } from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | Text,
6 | Image,
7 | Dimensions,
8 | ScrollView,
9 | } from 'react-native';
10 | import { Move } from 'chess.js';
11 | import Chessboard, { ChessboardRef } from '../../components/chessboard';
12 | import { PIECES } from '../../components/chessboard/constants';
13 | import { ChessMoveInfo } from '../../components/chessboard/context/props-context';
14 | import React from 'react';
15 |
16 | const SCREEN_WIDTH = Dimensions.get('window').width;
17 | const CHESSBOARD_SIZE = SCREEN_WIDTH;
18 | // Math.floor((SCREEN_WIDTH - PADDING - CHESSBOARD_BORDER_WIDTH) / 8) * 8;
19 |
20 | const ChessBoard = () => {
21 | const chessboardRef = useRef(null);
22 | const [moves, setMoves] = useState([]);
23 | const [pgn, setPgn] = useState('');
24 | const scrollViewRef = useRef(null);
25 |
26 | return (
27 |
35 |
42 |
46 | scrollViewRef.current.scrollToEnd({ animated: true })
47 | }
48 | >
49 |
57 | {moves.map((move, index) => {
58 | return (
59 |
60 | {index % 2 === 0 ? (
61 | {index / 2 + 1}.
62 | ) : null}
63 | {move.move.san}
64 |
65 | );
66 | })}
67 |
68 |
69 |
70 |
71 |
72 |
73 | {
76 | setMoves([...moves, move]);
77 | setPgn(move.state.pgn);
78 | }}
79 | durations={{ move: 0 }}
80 | boardSize={CHESSBOARD_SIZE}
81 | // fen={gameState?.board?.fen}
82 | boardOrientation={'black'}
83 | colors={{
84 | black: '#779654',
85 | white: '#efeed3',
86 | checkmateHighlight: '#ff0000',
87 | lastMoveHighlight: '#f7f683',
88 | }}
89 | />
90 |
91 |
92 |
93 | );
94 | };
95 |
96 | export default ChessBoard;
97 |
98 | const PlayerStrip = () => {
99 | return (
100 |
101 |
102 |
103 | Player 1
104 |
105 |
106 | );
107 | };
108 |
109 | function get_captured_pieces(moves: Move[], color: string) {
110 | const captured = { p: 0, n: 0, b: 0, r: 0, q: 0 };
111 |
112 | for (const move of moves) {
113 | if (move.hasOwnProperty('captured') && move.color !== color[0]) {
114 | captured[move.captured]++;
115 | }
116 | }
117 |
118 | return captured;
119 | }
120 |
121 | const CapturedPieces = memo(function CapturedPieces({
122 | moves,
123 | color,
124 | }: {
125 | moves: Move[];
126 | color: 'black' | 'white';
127 | }) {
128 | const captured = get_captured_pieces(moves, color);
129 |
130 | return (
131 |
132 | {Object.entries(captured).map((item) =>
133 | item[1] ? (
134 |
135 |
139 |
140 | {item[1]}
141 |
142 |
143 | ) : (
144 | <>>
145 | ),
146 | )}
147 |
148 | );
149 | });
150 |
151 | const styles = StyleSheet.create({
152 | chessboardContainer: {},
153 | playerStrip: {
154 | backgroundColor: '#302c2a',
155 | padding: 20,
156 | width: '100%',
157 | flexDirection: 'row',
158 | justifyContent: 'flex-start',
159 | alignItems: 'flex-start',
160 | gap: 8,
161 | },
162 | name: { color: '#fff', fontSize: 12, lineHeight: 12 },
163 | playerAvatar: {
164 | width: 32,
165 | height: 32,
166 | backgroundColor: '#e6e4e2',
167 | borderRadius: 4,
168 | },
169 | capturedPieceContainer: { flexDirection: 'row', gap: 1, width: '100%' },
170 | blueDot: {
171 | position: 'absolute',
172 | borderRadius: 8,
173 | bottom: 0,
174 | right: 0,
175 | width: 16,
176 | height: 16,
177 | justifyContent: 'center',
178 | alignItems: 'center',
179 |
180 | backgroundColor: '#0000ff',
181 | },
182 | });
183 |
--------------------------------------------------------------------------------
/apps/native/app/index.tsx:
--------------------------------------------------------------------------------
1 | import { Redirect } from 'expo-router';
2 |
3 | const Home = () => {
4 | return ;
5 | };
6 |
7 | export default Home;
8 |
--------------------------------------------------------------------------------
/apps/native/app/login/index.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, Image, StyleSheet, Pressable } from 'react-native';
2 | import { PRIMARY_BROWN } from '../../constants/colors';
3 | import { PIECES } from '../../components/chessboard/constants';
4 | import { Stack, router } from 'expo-router';
5 |
6 | const LoginScreen = () => {
7 | return (
8 |
9 |
10 | Create your
11 | 100XChess account
12 |
13 | {
16 | router.replace('game');
17 | }}
18 | >
19 |
20 | Continue as guest
21 |
22 |
23 | OR
24 |
25 |
26 | Continue with Google
27 |
28 |
29 |
30 |
31 | Continue with Github
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | const styles = StyleSheet.create({
39 | container: {
40 | backgroundColor: PRIMARY_BROWN,
41 | flex: 1,
42 | justifyContent: 'center',
43 | alignItems: 'center',
44 | paddingHorizontal: 16,
45 | paddingVertical: 24,
46 | },
47 | title: {
48 | color: '#FFF',
49 | fontSize: 24,
50 | fontWeight: '900',
51 | textAlign: 'center',
52 | },
53 | image: {
54 | width: 60,
55 | aspectRatio: 1,
56 | },
57 | button1: {
58 | backgroundColor: '#779654',
59 | padding: 12,
60 | margin: 10,
61 | borderRadius: 8,
62 | justifyContent: 'center',
63 | },
64 | button2: {
65 | backgroundColor: '#302c2a',
66 | padding: 12,
67 | margin: 10,
68 | borderRadius: 8,
69 | justifyContent: 'center',
70 | },
71 | loginText: {
72 | color: '#FFF',
73 | fontSize: 20,
74 | fontWeight: '900',
75 | textAlign: 'center',
76 | },
77 | loginText2: {
78 | color: '#FFF',
79 | fontSize: 16,
80 | fontWeight: '600',
81 | textAlign: 'center',
82 | },
83 | });
84 |
85 | export default LoginScreen;
86 |
--------------------------------------------------------------------------------
/apps/native/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/apps/native/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/assets/favicon.png
--------------------------------------------------------------------------------
/apps/native/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/assets/icon.png
--------------------------------------------------------------------------------
/apps/native/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/assets/splash.png
--------------------------------------------------------------------------------
/apps/native/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/bb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/bb.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/bk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/bk.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/bn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/bn.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/bp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/bp.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/bq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/bq.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/br.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/br.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/wb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/wb.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/wk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/wk.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/wn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/wn.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/wp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/wp.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/wq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/wq.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/assets/wr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/chess/e718e1376fa44d7735ede0c9d2b9e5c6ea7175bb/apps/native/components/chessboard/assets/wr.png
--------------------------------------------------------------------------------
/apps/native/components/chessboard/components/chessboard-background.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-native/no-inline-styles */
2 | import React from 'react';
3 | import { View, StyleSheet, Text } from 'react-native';
4 | import { useChessboardProps } from '../context/props-context/hooks';
5 |
6 | const styles = StyleSheet.create({
7 | container: {
8 | flex: 1,
9 | flexDirection: 'row',
10 | },
11 | });
12 |
13 | type BackgroundProps = {
14 | letters: boolean;
15 | numbers: boolean;
16 | };
17 |
18 | interface BaseProps extends BackgroundProps {
19 | white: boolean;
20 | }
21 |
22 | interface RowProps extends BaseProps {
23 | row: number;
24 | }
25 |
26 | interface SquareProps extends RowProps {
27 | col: number;
28 | }
29 |
30 | const Square = React.memo(
31 | ({ white, row, col, letters, numbers }: SquareProps) => {
32 | const { colors, boardOrientation } = useChessboardProps();
33 | const backgroundColor = white ? colors.black : colors.white;
34 | const color = white ? colors.white : colors.black;
35 | const textStyle = { fontWeight: '500' as const, fontSize: 10, color };
36 | const newLocal = col === 0;
37 | const rowNumber = boardOrientation === 'white' ? 8 - row : row + 1;
38 | const colNumber = boardOrientation === 'white' ? col : 7 - col;
39 | return (
40 |
48 | {numbers && (
49 |
50 | {'' + rowNumber}
51 |
52 | )}
53 | {row === 7 && letters && (
54 |
55 | {String.fromCharCode(97 + colNumber)}
56 |
57 | )}
58 |
59 | );
60 | },
61 | );
62 |
63 | const Row = React.memo(({ white, row, ...rest }: RowProps) => {
64 | const offset = white ? 0 : 1;
65 | return (
66 |
67 | {new Array(8).fill(0).map((_, i) => (
68 |
75 | ))}
76 |
77 | );
78 | });
79 |
80 | const Background: React.FC = React.memo(() => {
81 | const { withLetters, withNumbers, boardOrientation } = useChessboardProps();
82 | return (
83 |
91 | {new Array(8).fill(0).map((_, i) => (
92 |
99 | ))}
100 |
101 | );
102 | });
103 |
104 | export default Background;
105 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/components/highlighted-squares/highlighted-square.tsx:
--------------------------------------------------------------------------------
1 | import React, { useImperativeHandle } from 'react';
2 | import { StyleProp, StyleSheet, ViewStyle } from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withTiming,
7 | } from 'react-native-reanimated';
8 | import { useChessboardProps } from '../../context/props-context/hooks';
9 |
10 | type HighlightedSquareProps = {
11 | style?: StyleProp;
12 | };
13 |
14 | export type HighlightedSquareRefType = {
15 | isHighlighted: () => boolean;
16 | reset: () => void;
17 | highlight: (_?: { backgroundColor?: string }) => void;
18 | };
19 |
20 | const HighlightedSquareComponent = React.forwardRef<
21 | HighlightedSquareRefType,
22 | HighlightedSquareProps
23 | >(({ style }, ref) => {
24 | const {
25 | colors: { lastMoveHighlight },
26 | } = useChessboardProps();
27 | const backgroundColor = useSharedValue(lastMoveHighlight);
28 | const isHighlighted = useSharedValue(false);
29 |
30 | useImperativeHandle(
31 | ref,
32 | () => ({
33 | reset: () => {
34 | isHighlighted.value = false;
35 | },
36 | highlight: ({ backgroundColor: bg } = {}) => {
37 | backgroundColor.value = bg ?? lastMoveHighlight;
38 | isHighlighted.value = true;
39 | },
40 | isHighlighted: () => isHighlighted.value,
41 | }),
42 | [backgroundColor, isHighlighted, lastMoveHighlight],
43 | );
44 |
45 | const rHighlightedSquareStyle = useAnimatedStyle(() => {
46 | return {
47 | opacity: withTiming(isHighlighted.value ? 1 : 0),
48 | backgroundColor: backgroundColor.value,
49 | };
50 | }, []);
51 |
52 | return (
53 |
56 | );
57 | });
58 |
59 | const styles = StyleSheet.create({
60 | highlightedSquare: {
61 | position: 'absolute',
62 | aspectRatio: 1,
63 | },
64 | });
65 |
66 | const HighlightedSquare = React.memo(HighlightedSquareComponent);
67 |
68 | export { HighlightedSquare };
69 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/components/highlighted-squares/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 |
4 | import { useChessboardProps } from '../../context/props-context/hooks';
5 |
6 | import { useChessEngine } from '../../context/chess-engine-context/hooks';
7 | import { useReversePiecePosition } from '../../notation';
8 | import { HighlightedSquare } from './highlighted-square';
9 | import { useSquareRefs } from '../../context/board-refs-context/hooks';
10 |
11 | const HighlightedSquares: React.FC = React.memo(() => {
12 | const chess = useChessEngine();
13 | const board = useMemo(() => chess.board(), [chess]);
14 | const { pieceSize } = useChessboardProps();
15 | const { toPosition, toTranslation } = useReversePiecePosition();
16 | const refs = useSquareRefs();
17 |
18 | return (
19 |
24 | {board.map((row, y) =>
25 | row.map((_, x) => {
26 | const square = toPosition({ x: x * pieceSize, y: y * pieceSize });
27 | const translation = toTranslation(square);
28 |
29 | return (
30 |
44 | );
45 | }),
46 | )}
47 |
48 | );
49 | });
50 |
51 | const styles = StyleSheet.create({
52 | highlightedSquare: {
53 | position: 'absolute',
54 | aspectRatio: 1,
55 | },
56 | });
57 |
58 | export { HighlightedSquares };
59 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/components/piece/visual-piece.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image, ImageProps } from 'react-native';
3 | import { PIECES } from '../../constants';
4 | import { useChessboardProps } from '../../context/props-context/hooks';
5 | import type { PieceType } from '../../types';
6 |
7 | type ChessPieceType = {
8 | id: PieceType;
9 | } & Partial;
10 |
11 | const ChessPiece: React.FC = React.memo(({ id, ...rest }) => {
12 | const { pieceSize, renderPiece, boardOrientation } = useChessboardProps();
13 |
14 | return (
15 | renderPiece?.(id) ?? (
16 |
30 | )
31 | );
32 | });
33 |
34 | export { ChessPiece };
35 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/components/pieces.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useChessboardProps } from '../context/props-context/hooks';
3 |
4 | import { useBoard } from '../context/board-context';
5 | import { usePieceRefs } from '../context/board-refs-context/hooks';
6 |
7 | import Piece from './piece';
8 | import { useReversePiecePosition } from '../notation';
9 |
10 | const Pieces = React.memo(() => {
11 | const board = useBoard();
12 | const refs = usePieceRefs();
13 | const { pieceSize } = useChessboardProps();
14 | const { toPosition } = useReversePiecePosition();
15 |
16 | return (
17 | <>
18 | {board.map((row, y) =>
19 | row.map((piece, x) => {
20 | if (piece !== null) {
21 | const square = toPosition({
22 | x: x * pieceSize,
23 | y: y * pieceSize,
24 | });
25 |
26 | return (
27 |
35 | );
36 | }
37 | return null;
38 | }),
39 | )}
40 | >
41 | );
42 | });
43 |
44 | export { Pieces };
45 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/components/suggested-dots/PlaceholderDot.tsx:
--------------------------------------------------------------------------------
1 | import type { Square } from 'chess.js';
2 | import React, { useMemo } from 'react';
3 | import { StyleSheet, View } from 'react-native';
4 | import Animated, {
5 | runOnJS,
6 | useAnimatedStyle,
7 | useDerivedValue,
8 | withTiming,
9 | } from 'react-native-reanimated';
10 | import { useChessboardProps } from '../../context/props-context/hooks';
11 |
12 | import { useReversePiecePosition } from '../../notation';
13 |
14 | type PlaceholderDotProps = {
15 | x: number;
16 | y: number;
17 | selectableSquares: Animated.SharedValue;
18 | moveTo?: (to: Square) => void;
19 | };
20 |
21 | const PlaceholderDot: React.FC = React.memo(
22 | ({ x, y, selectableSquares, moveTo }) => {
23 | const { pieceSize } = useChessboardProps();
24 | const { toPosition, toTranslation } = useReversePiecePosition();
25 |
26 | const currentSquare = toPosition({ x: x * pieceSize, y: y * pieceSize });
27 | const translation = useMemo(
28 | () => toTranslation(currentSquare),
29 | [currentSquare, toTranslation],
30 | );
31 |
32 | const isSelectable = useDerivedValue(() => {
33 | 'worklet';
34 | return (
35 | selectableSquares.value
36 | .map((square) => square.includes(currentSquare))
37 | .filter((v) => v).length > 0
38 | );
39 | }, [currentSquare, selectableSquares.value]);
40 |
41 | const rPlaceholderStyle = useAnimatedStyle(() => {
42 | const canBeSelected = isSelectable.value;
43 | return { opacity: withTiming(canBeSelected ? 0.15 : 0) };
44 | }, []);
45 |
46 | return (
47 | {
49 | if (isSelectable.value && moveTo) {
50 | runOnJS(moveTo)(currentSquare);
51 | }
52 | }}
53 | style={[
54 | styles.placeholderContainer,
55 | {
56 | width: pieceSize,
57 | padding: pieceSize / 3.2,
58 | transform: [
59 | { translateX: translation.x },
60 | { translateY: translation.y },
61 | ],
62 | },
63 | ]}
64 | >
65 |
72 |
73 | );
74 | },
75 | );
76 |
77 | const styles = StyleSheet.create({
78 | placeholderContainer: {
79 | position: 'absolute',
80 | aspectRatio: 1,
81 | backgroundColor: 'transparent',
82 | },
83 | placeholder: {
84 | flex: 1,
85 | backgroundColor: 'black',
86 | opacity: 0.2,
87 | },
88 | });
89 |
90 | export { PlaceholderDot };
91 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/components/suggested-dots/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 |
4 | import { useBoardOperations } from '../../context/board-operations-context/hooks';
5 | import { useChessEngine } from '../../context/chess-engine-context/hooks';
6 |
7 | import { PlaceholderDot } from './PlaceholderDot';
8 |
9 | const SuggestedDots: React.FC = React.memo(() => {
10 | const chess = useChessEngine();
11 | const { moveTo, selectableSquares } = useBoardOperations();
12 | const board = useMemo(() => chess.board(), [chess]);
13 |
14 | return (
15 |
20 | {board.map((row, y) =>
21 | row.map((_, x) => {
22 | return (
23 |
30 | );
31 | }),
32 | )}
33 |
34 | );
35 | });
36 |
37 | export { SuggestedDots };
38 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/constants.ts:
--------------------------------------------------------------------------------
1 | import type { PiecesType } from './types';
2 |
3 | const PIECES: PiecesType = {
4 | br: require('./assets/br.png'),
5 | bp: require('./assets/bp.png'),
6 | bn: require('./assets/bn.png'),
7 | bb: require('./assets/bb.png'),
8 | bq: require('./assets/bq.png'),
9 | bk: require('./assets/bk.png'),
10 | wr: require('./assets/wr.png'),
11 | wn: require('./assets/wn.png'),
12 | wb: require('./assets/wb.png'),
13 | wq: require('./assets/wq.png'),
14 | wk: require('./assets/wk.png'),
15 | wp: require('./assets/wp.png'),
16 | };
17 |
18 | const assets = Object.values(PIECES);
19 |
20 | export { assets, PIECES };
21 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-context-provider.tsx:
--------------------------------------------------------------------------------
1 | import { Chess } from 'chess.js';
2 | import React, { useImperativeHandle, useMemo, useRef, useState } from 'react';
3 | import type { ChessboardState } from '../helpers/get-chessboard-state';
4 | import { useConst } from '../hooks/use-const';
5 |
6 | import { BoardContext, BoardSetterContext } from './board-context';
7 | import {
8 | BoardOperationsContextProvider,
9 | BoardOperationsRef,
10 | } from './board-operations-context';
11 | import { BoardPromotionContextProvider } from './board-promotion-context';
12 | import { BoardRefsContextProvider, ChessboardRef } from './board-refs-context';
13 | import { ChessEngineContext } from './chess-engine-context';
14 |
15 | type BoardContextProviderProps = {
16 | fen?: string;
17 | children?: React.ReactNode;
18 | };
19 |
20 | const ChessboardContextProviderComponent = React.forwardRef<
21 | ChessboardRef,
22 | BoardContextProviderProps
23 | >(({ children, fen }, ref) => {
24 | const chess = useConst(() => new Chess(fen));
25 | const chessboardRef = useRef(null);
26 | const boardOperationsRef = useRef(null);
27 |
28 | const [board, setBoard] = useState(chess.board());
29 |
30 | const chessboardController: ChessboardRef = useMemo(() => {
31 | return {
32 | move: (params) => chessboardRef.current?.move?.(params),
33 | highlight: (params) => chessboardRef.current?.highlight(params),
34 | resetAllHighlightedSquares: () =>
35 | chessboardRef.current?.resetAllHighlightedSquares(),
36 | getState: () => chessboardRef?.current?.getState() as ChessboardState,
37 | resetBoard: (params) => {
38 | chessboardRef.current?.resetBoard(params);
39 | boardOperationsRef.current?.reset();
40 | },
41 | };
42 | }, []);
43 |
44 | useImperativeHandle(ref, () => chessboardController, [chessboardController]);
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
56 | {children}
57 |
58 |
59 |
60 |
61 |
62 |
63 | );
64 | });
65 |
66 | const ChessboardContextProvider = React.memo(
67 | ChessboardContextProviderComponent,
68 | );
69 | export {
70 | ChessboardContextProvider,
71 | ChessEngineContext,
72 | BoardContext,
73 | BoardSetterContext,
74 | };
75 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-context/index.ts:
--------------------------------------------------------------------------------
1 | import type { ChessInstance, PieceType } from 'chess.js';
2 | import React, { createContext } from 'react';
3 | import type { Player } from '../../types';
4 | import { useContext } from 'react';
5 |
6 | const BoardContext = createContext>(
7 | {} as any,
8 | );
9 |
10 | const BoardSetterContext = createContext<
11 | React.Dispatch<
12 | React.SetStateAction<
13 | ({
14 | type: PieceType;
15 | color: Player;
16 | } | null)[][]
17 | >
18 | >
19 | >({} as any);
20 |
21 | const useBoard = () => {
22 | return useContext(BoardContext);
23 | };
24 |
25 | const useSetBoard = () => {
26 | return useContext(BoardSetterContext);
27 | };
28 |
29 | export { BoardContext, BoardSetterContext, useBoard, useSetBoard };
30 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-operations-context/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { BoardOperationsContext } from './index';
4 |
5 | const useBoardOperations = () => {
6 | return useContext(BoardOperationsContext);
7 | };
8 |
9 | export { useBoardOperations };
10 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-promotion-context/dialog/dialog-piece.tsx:
--------------------------------------------------------------------------------
1 | import type { PieceType } from 'chess.js';
2 | import React from 'react';
3 | import { StyleSheet, View } from 'react-native';
4 | import { Gesture, GestureDetector } from 'react-native-gesture-handler';
5 | import Animated, {
6 | runOnJS,
7 | useAnimatedStyle,
8 | useSharedValue,
9 | withTiming,
10 | } from 'react-native-reanimated';
11 | import type { Player } from '../../../types';
12 | import { ChessPiece } from '../../../components/piece/visual-piece';
13 | import { useChessboardProps } from '../../../context/props-context/hooks';
14 |
15 | type DialogPieceProps = {
16 | index: number;
17 | width: number;
18 | type: Player;
19 | piece: PieceType;
20 | onSelectPiece?: (piece: PieceType) => void;
21 | };
22 |
23 | const DialogPiece: React.FC = React.memo(
24 | ({ index, width, type, piece, onSelectPiece }) => {
25 | const isTapActive = useSharedValue(false);
26 | const {
27 | colors: { promotionPieceButton },
28 | } = useChessboardProps();
29 |
30 | const gesture = Gesture.Tap()
31 | .onBegin(() => {
32 | isTapActive.value = true;
33 | })
34 | .onTouchesUp(() => {
35 | if (onSelectPiece) runOnJS(onSelectPiece)(piece);
36 | })
37 | .onFinalize(() => {
38 | isTapActive.value = false;
39 | })
40 | .shouldCancelWhenOutside(true)
41 | .maxDuration(10000);
42 |
43 | const rStyle = useAnimatedStyle(() => {
44 | return {
45 | opacity: withTiming(isTapActive.value ? 1 : 0, { duration: 150 }),
46 | };
47 | }, []);
48 |
49 | return (
50 |
51 |
52 |
67 |
77 |
78 |
79 |
80 |
81 | );
82 | },
83 | );
84 |
85 | const styles = StyleSheet.create({
86 | pieceContainer: {
87 | aspectRatio: 1,
88 | justifyContent: 'center',
89 | alignItems: 'center',
90 | borderBottomWidth: 0,
91 | borderColor: 'rgba(0,0,0,0.2)',
92 | },
93 | });
94 |
95 | export { DialogPiece };
96 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-promotion-context/dialog/index.tsx:
--------------------------------------------------------------------------------
1 | import { useChessboardProps } from '../../../context/props-context/hooks';
2 | import React from 'react';
3 | import type { PieceType } from 'chess.js';
4 | import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
5 |
6 | import { StyleSheet } from 'react-native';
7 | import type { BoardPromotionContextState } from '..';
8 | import { DialogPiece } from './dialog-piece';
9 |
10 | const PROMOTION_PIECES: PieceType[] = ['q', 'r', 'n', 'b'];
11 |
12 | const PromotionDialog: React.FC> =
13 | React.memo(({ type, onSelect }) => {
14 | const { boardSize } = useChessboardProps();
15 |
16 | return (
17 |
27 | {PROMOTION_PIECES.map((piece, i) => {
28 | return (
29 |
37 | );
38 | })}
39 |
40 | );
41 | });
42 |
43 | const styles = StyleSheet.create({
44 | container: {
45 | position: 'absolute',
46 | aspectRatio: 1,
47 | backgroundColor: 'rgba(256,256,256,0.85)',
48 | borderRadius: 5,
49 | zIndex: 10,
50 | justifyContent: 'center',
51 | alignItems: 'center',
52 | shadowColor: 'black',
53 | shadowOpacity: 0.2,
54 | shadowOffset: {
55 | height: 5,
56 | width: 0,
57 | },
58 | flexWrap: 'wrap',
59 | },
60 | });
61 |
62 | export { PromotionDialog };
63 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-promotion-context/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { BoardPromotionContext } from './index';
3 |
4 | const useBoardPromotion = () => useContext(BoardPromotionContext);
5 |
6 | export { useBoardPromotion, BoardPromotionContext };
7 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-promotion-context/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, useState } from 'react';
2 | import type { ChessInstance, PieceType } from 'chess.js';
3 | import { PromotionDialog } from './dialog';
4 |
5 | export type BoardPromotionContextType = {
6 | showPromotionDialog: (_: {
7 | type: PromotionDialogType;
8 | onSelect?: (_: PieceType) => void;
9 | }) => void;
10 | isPromoting: boolean;
11 | };
12 |
13 | const BoardPromotionContext = React.createContext({
14 | showPromotionDialog: () => {
15 | //
16 | },
17 | isPromoting: false,
18 | });
19 |
20 | type PromotionDialogType = ReturnType;
21 |
22 | export type BoardPromotionContextState = {
23 | isDialogActive: boolean;
24 | type?: PromotionDialogType;
25 | onSelect?: (_: PieceType) => void;
26 | };
27 |
28 | const BoardPromotionContextProvider: React.FC = React.memo(({ children }) => {
29 | const [dialog, setDialog] = useState({
30 | isDialogActive: false,
31 | });
32 |
33 | const showPromotionDialog: BoardPromotionContextType['showPromotionDialog'] =
34 | useCallback(({ type, onSelect }) => {
35 | setDialog({ isDialogActive: true, type, onSelect });
36 | }, []);
37 |
38 | const onSelect = useCallback(
39 | (piece: PieceType) => {
40 | dialog.onSelect?.(piece);
41 | setDialog({ isDialogActive: false });
42 | },
43 | [dialog],
44 | );
45 |
46 | const value = useMemo(
47 | () => ({
48 | showPromotionDialog,
49 | isPromoting: dialog.isDialogActive,
50 | }),
51 | [dialog.isDialogActive, showPromotionDialog],
52 | );
53 |
54 | return (
55 |
56 | {dialog.isDialogActive && (
57 |
58 | )}
59 | {children}
60 |
61 | );
62 | });
63 |
64 | export { BoardPromotionContextProvider, BoardPromotionContext };
65 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-refs-context/contexts.ts:
--------------------------------------------------------------------------------
1 | import type { Square } from 'chess.js';
2 | import React, { createContext } from 'react';
3 |
4 | import type { ChessPieceRef } from '../../components/piece';
5 | import type { HighlightedSquareRefType } from '../../components/highlighted-squares/highlighted-square';
6 |
7 | const PieceRefsContext = createContext
10 | > | null> | null>(null);
11 |
12 | const SquareRefsContext = createContext
15 | > | null> | null>(null);
16 |
17 | export { PieceRefsContext, SquareRefsContext };
18 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-refs-context/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { PieceRefsContext, SquareRefsContext } from './contexts';
4 |
5 | export const usePieceRefs = () => {
6 | return useContext(PieceRefsContext);
7 | };
8 |
9 | export const useSquareRefs = () => {
10 | return useContext(SquareRefsContext);
11 | };
12 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/board-refs-context/index.tsx:
--------------------------------------------------------------------------------
1 | import type { Move, Square } from 'chess.js';
2 | import React, {
3 | createContext,
4 | useCallback,
5 | useImperativeHandle,
6 | useRef,
7 | } from 'react';
8 | import {
9 | ChessboardState,
10 | getChessboardState,
11 | } from '../../helpers/get-chessboard-state';
12 | import type { ChessPieceRef } from '../../components/piece';
13 | import type { HighlightedSquareRefType } from '../../components/highlighted-squares/highlighted-square';
14 |
15 | import { useChessEngine } from '../chess-engine-context/hooks';
16 | import { PieceRefsContext, SquareRefsContext } from './contexts';
17 | import { useSetBoard } from '../board-context';
18 |
19 | export type ChessboardRef = {
20 | move: (_: {
21 | from: Square;
22 | to: Square;
23 | }) => Promise | undefined;
24 | highlight: (_: { square: Square; color?: string }) => void;
25 | resetAllHighlightedSquares: () => void;
26 | resetBoard: (fen?: string) => void;
27 | getState: () => ChessboardState;
28 | };
29 |
30 | const BoardRefsContextProviderComponent = React.forwardRef<
31 | ChessboardRef,
32 | { children?: React.ReactNode }
33 | >(({ children }, ref) => {
34 | const chess = useChessEngine();
35 | const board = chess.board();
36 | const setBoard = useSetBoard();
37 |
38 | // There must be a better way of doing this.
39 | const generateBoardRefs = useCallback(() => {
40 | let acc = {};
41 | for (let x = 0; x < board.length; x++) {
42 | const row = board[x];
43 | for (let y = 0; y < row.length; y++) {
44 | const col = String.fromCharCode(97 + Math.round(x));
45 | // eslint-disable-next-line no-shadow
46 | const row = `${8 - Math.round(y)}`;
47 | const square = `${col}${row}` as Square;
48 |
49 | // eslint-disable-next-line react-hooks/rules-of-hooks
50 | acc = { ...acc, [square]: useRef(null) };
51 | }
52 | }
53 | return acc as any;
54 | }, [board]);
55 |
56 | const pieceRefs: React.MutableRefObject
59 | > | null> = useRef(generateBoardRefs());
60 |
61 | const squareRefs: React.MutableRefObject
64 | > | null> = useRef(generateBoardRefs());
65 |
66 | useImperativeHandle(
67 | ref,
68 | () => ({
69 | move: ({ from, to }) => {
70 | return pieceRefs?.current?.[from].current?.moveTo?.(to);
71 | },
72 | highlight: ({ square, color }) => {
73 | squareRefs.current?.[square].current.highlight({
74 | backgroundColor: color,
75 | });
76 | },
77 | resetAllHighlightedSquares: () => {
78 | for (let x = 0; x < board.length; x++) {
79 | const row = board[x];
80 | for (let y = 0; y < row.length; y++) {
81 | const col = String.fromCharCode(97 + Math.round(x));
82 | // eslint-disable-next-line no-shadow
83 | const row = `${8 - Math.round(y)}`;
84 | const square = `${col}${row}` as Square;
85 | squareRefs.current?.[square].current.reset();
86 | }
87 | }
88 | },
89 | getState: () => {
90 | return getChessboardState(chess);
91 | },
92 | resetBoard: (fen) => {
93 | chess.reset();
94 | if (fen) chess.load(fen);
95 | setBoard(chess.board());
96 | },
97 | }),
98 | [board, chess, setBoard],
99 | );
100 |
101 | return (
102 |
103 |
104 | {children}
105 |
106 |
107 | );
108 | });
109 |
110 | const BoardRefsContextProvider = React.memo(BoardRefsContextProviderComponent);
111 |
112 | export { BoardRefsContextProvider };
113 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/chess-engine-context/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { ChessEngineContext } from './index';
4 |
5 | export const useChessEngine = () => {
6 | return useContext(ChessEngineContext);
7 | };
8 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/chess-engine-context/index.ts:
--------------------------------------------------------------------------------
1 | import type { ChessInstance } from 'chess.js';
2 | import { createContext } from 'react';
3 |
4 | const ChessEngineContext = createContext({} as any);
5 |
6 | export { ChessEngineContext };
7 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/props-context/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { ChessboardPropsContext } from './index';
3 |
4 | export const useChessboardProps = () => useContext(ChessboardPropsContext);
5 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/context/props-context/index.tsx:
--------------------------------------------------------------------------------
1 | import type { Move } from 'chess.js';
2 | import React, { useMemo } from 'react';
3 | import { Dimensions } from 'react-native';
4 | import type { PieceType } from '../../types';
5 |
6 | import type { ChessboardState } from '../../helpers/get-chessboard-state';
7 |
8 | export type ChessMoveInfo = {
9 | move: Move;
10 | state: ChessboardState & { in_promotion: boolean };
11 | };
12 |
13 | type ChessboardColorsType = {
14 | white: string;
15 | black: string;
16 | lastMoveHighlight?: string;
17 | checkmateHighlight?: string;
18 | promotionPieceButton?: string;
19 | };
20 |
21 | type ChessboardDurationsType = {
22 | move?: number;
23 | };
24 |
25 | type ChessboardProps = {
26 | /**
27 | * Enables gestures for chess pieces.
28 | */
29 | gestureEnabled?: boolean;
30 | /**
31 | * Indicates the initial fen position of the chessboard.
32 | */
33 | fen?: string;
34 | /**
35 | * Decides whether or not to show the letters on the bottom horizontal axis of the chessboard.
36 | */
37 | withLetters?: boolean;
38 | /**
39 | * Decides whether or not to show the letters on the bottom horizontal axis of the chessboard.
40 | */
41 | withNumbers?: boolean;
42 | /**
43 | * Indicates the chessboard width and height.
44 | */
45 | boardSize?: number;
46 | /**
47 | *
48 | * It gives the possibility to customise the chessboard pieces.
49 | *
50 | * In detail, it takes a PieceType as input, which is constructed as follows:
51 | */
52 | renderPiece?: (piece: PieceType) => React.ReactElement | null;
53 | /**
54 | * It's a particularly useful callback if you want to execute an instruction after a move.
55 | */
56 | onMove?: (info: ChessMoveInfo) => void;
57 | /**
58 | * Useful if you want to customise the default colors used in the chessboard.
59 | */
60 | colors?: ChessboardColorsType;
61 | /**
62 | * Useful if you want to customise the default durations used in the chessboard (in milliseconds).
63 | */
64 | durations?: ChessboardDurationsType;
65 |
66 | boardOrientation?: 'white' | 'black';
67 | };
68 |
69 | type ChessboardContextType = ChessboardProps &
70 | Required<
71 | Pick<
72 | ChessboardProps,
73 | 'gestureEnabled' | 'withLetters' | 'withNumbers' | 'boardSize'
74 | >
75 | > & { pieceSize: number } & {
76 | colors: Required;
77 | durations: Required;
78 | };
79 |
80 | const { width: SCREEN_WIDTH } = Dimensions.get('window');
81 | const DEFAULT_BOARD_SIZE = Math.floor(SCREEN_WIDTH / 8) * 8;
82 |
83 | const defaultChessboardProps: ChessboardContextType = {
84 | gestureEnabled: true,
85 | colors: {
86 | black: '#62B1A8',
87 | white: '#D9FDF8',
88 | lastMoveHighlight: 'rgba(255,255,0, 0.5)',
89 | checkmateHighlight: '#E84855',
90 | promotionPieceButton: '#FF9B71',
91 | },
92 | durations: {
93 | move: 150,
94 | },
95 | withLetters: true,
96 | withNumbers: true,
97 | boardSize: DEFAULT_BOARD_SIZE,
98 | pieceSize: DEFAULT_BOARD_SIZE / 8,
99 | boardOrientation: 'white',
100 | };
101 |
102 | const ChessboardPropsContext = React.createContext(
103 | defaultChessboardProps,
104 | );
105 |
106 | const ChessboardPropsContextProvider: React.FC<
107 | { children: React.ReactNode } & ChessboardProps
108 | > = React.memo(({ children, ...rest }) => {
109 | const value = useMemo(() => {
110 | const data = {
111 | ...defaultChessboardProps,
112 | ...rest,
113 | colors: { ...defaultChessboardProps.colors, ...rest.colors },
114 | durations: { ...defaultChessboardProps.durations, ...rest.durations },
115 | };
116 | return { ...data, pieceSize: data.boardSize / 8 };
117 | }, [rest]);
118 |
119 | return (
120 |
121 | {children}
122 |
123 | );
124 | });
125 |
126 | export { ChessboardPropsContextProvider, ChessboardPropsContext };
127 | // eslint-disable-next-line no-undef
128 | export type { ChessboardProps };
129 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/helpers/get-chessboard-state.ts:
--------------------------------------------------------------------------------
1 | import type { ChessInstance } from 'chess.js';
2 |
3 | type ChessboardStateFunctions = Pick<
4 | ChessInstance,
5 | | 'in_check'
6 | | 'in_checkmate'
7 | | 'in_draw'
8 | | 'in_stalemate'
9 | | 'in_threefold_repetition'
10 | | 'insufficient_material'
11 | | 'game_over'
12 | | 'fen'
13 | | 'pgn'
14 | >;
15 |
16 | type RecordReturnTypes = {
17 | readonly [P in keyof T]: T[P] extends () => any ? ReturnType : T[P];
18 | };
19 |
20 | export type ChessboardState = RecordReturnTypes;
21 |
22 | export const getChessboardState = (chess: ChessInstance): ChessboardState => {
23 | return {
24 | in_check: chess.in_check(),
25 | in_checkmate: chess.in_checkmate(),
26 | in_draw: chess.in_draw(),
27 | in_stalemate: chess.in_stalemate(),
28 | in_threefold_repetition: chess.in_threefold_repetition(),
29 | insufficient_material: chess.insufficient_material(),
30 | game_over: chess.game_over(),
31 | fen: chess.fen(),
32 | pgn: chess.pgn(),
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/hooks/use-const.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 |
3 | function useConst(initialValue: T | (() => T)): T {
4 | const ref = useRef<{ value: T }>();
5 | if (ref.current === undefined) {
6 | // Box the value in an object so we can tell if it's initialized even if the initializer
7 | // returns/is undefined
8 | ref.current = {
9 | value:
10 | typeof initialValue === 'function'
11 | ? (initialValue as Function)()
12 | : initialValue,
13 | };
14 | }
15 | return ref.current.value;
16 | }
17 |
18 | export { useConst };
19 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useImperativeHandle, useRef } from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 |
4 | import Background from './components/chessboard-background';
5 | import { HighlightedSquares } from './components/highlighted-squares';
6 | import { Pieces } from './components/pieces';
7 | import { SuggestedDots } from './components/suggested-dots';
8 | import { ChessboardContextProvider } from './context/board-context-provider';
9 | import type { ChessboardRef } from './context/board-refs-context';
10 | import {
11 | ChessboardProps,
12 | ChessboardPropsContextProvider,
13 | } from './context/props-context';
14 | import { useChessboardProps } from './context/props-context/hooks';
15 | import type { ChessboardState } from './helpers/get-chessboard-state';
16 |
17 | const styles = StyleSheet.create({
18 | container: {
19 | aspectRatio: 1,
20 | },
21 | });
22 |
23 | const Chessboard: React.FC = React.memo(() => {
24 | const { boardSize, boardOrientation } = useChessboardProps();
25 |
26 | return (
27 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | });
45 |
46 | const ChessboardContainerComponent = React.forwardRef<
47 | ChessboardRef,
48 | ChessboardProps
49 | >((props, ref) => {
50 | const chessboardRef = useRef(null);
51 |
52 | useImperativeHandle(
53 | ref,
54 | () => ({
55 | move: (params) => chessboardRef.current?.move?.(params),
56 | highlight: (params) => chessboardRef.current?.highlight(params),
57 | resetAllHighlightedSquares: () =>
58 | chessboardRef.current?.resetAllHighlightedSquares(),
59 | getState: () => chessboardRef?.current?.getState() as ChessboardState,
60 | resetBoard: (params) => chessboardRef.current?.resetBoard(params),
61 | }),
62 | [],
63 | );
64 |
65 | return (
66 |
67 |
68 |
69 |
70 |
71 | );
72 | });
73 |
74 | const ChessboardContainer = React.memo(ChessboardContainerComponent);
75 |
76 | export type { ChessboardRef };
77 | export default ChessboardContainer;
78 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/notation.ts:
--------------------------------------------------------------------------------
1 | import type { Square } from 'chess.js';
2 | import { useCallback } from 'react';
3 | import { useChessboardProps } from './context/props-context/hooks';
4 |
5 | import type { Vector } from './types';
6 |
7 | const useReversePiecePosition = () => {
8 | const { pieceSize } = useChessboardProps();
9 |
10 | const toTranslation = useCallback(
11 | (to: Square) => {
12 | 'worklet';
13 | const tokens = to.split('');
14 | const col = tokens[0];
15 | const row = tokens[1];
16 | if (!col || !row) {
17 | throw new Error('Invalid notation: ' + to);
18 | }
19 | const indexes = {
20 | x: col.charCodeAt(0) - 'a'.charCodeAt(0),
21 | y: parseInt(row, 10) - 1,
22 | };
23 | return {
24 | x: indexes.x * pieceSize,
25 | y: 7 * pieceSize - indexes.y * pieceSize,
26 | };
27 | },
28 | [pieceSize],
29 | );
30 |
31 | const toPosition = useCallback(
32 | ({ x, y }: Vector) => {
33 | 'worklet';
34 | const col = String.fromCharCode(97 + Math.round(x / pieceSize));
35 | const row = `${8 - Math.round(y / pieceSize)}`;
36 | return `${col}${row}` as Square;
37 | },
38 | [pieceSize],
39 | );
40 |
41 | return { toPosition, toTranslation };
42 | };
43 |
44 | export { useReversePiecePosition };
45 |
--------------------------------------------------------------------------------
/apps/native/components/chessboard/types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | import type { ChessInstance, Square } from 'chess.js';
4 |
5 | type Player = ReturnType;
6 | type Type = 'q' | 'r' | 'n' | 'b' | 'k' | 'p';
7 | type PieceType = `${Player}${Type}`;
8 |
9 | type PiecesType = Record>;
10 | type Vector = {
11 | x: T;
12 | y: T;
13 | };
14 |
15 | type ChessMove = {
16 | from: Square;
17 | to: Square;
18 | };
19 |
20 | type MoveType = { from: Square; to: Square };
21 |
22 | export type {
23 | Player,
24 | Type,
25 | PieceType,
26 | PiecesType,
27 | Vector,
28 | ChessMove,
29 | MoveType,
30 | };
31 |
--------------------------------------------------------------------------------
/apps/native/constants/colors.ts:
--------------------------------------------------------------------------------
1 | export const PRIMARY_BROWN = '#252422';
2 |
--------------------------------------------------------------------------------
/apps/native/hooks/useSocket.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | // import { useUser } from "@repo/store/useUser";
3 |
4 | const WS_URL = 'wss://chess-ws.100xdevs.com/';
5 |
6 | const user = {
7 | token: '1234kkiioo',
8 | };
9 |
10 | export const useSocket = () => {
11 | const [socket, setSocket] = useState(null);
12 | // const user = useUser();
13 |
14 | useEffect(() => {
15 | const ws = new WebSocket(`${WS_URL}?token=${user.token}`);
16 |
17 | ws.onopen = () => {
18 | setSocket(ws);
19 | };
20 |
21 | ws.onclose = () => {
22 | setSocket(null);
23 | };
24 |
25 | return () => {
26 | ws.close();
27 | };
28 | }, [user]);
29 |
30 | return socket;
31 | };
32 |
--------------------------------------------------------------------------------
/apps/native/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chessnative",
3 | "version": "1.0.0",
4 | "main": "expo-router/entry",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@types/react": "~18.2.45",
13 | "chess.js": "^0.12.0",
14 | "expo": "~50.0.14",
15 | "expo-constants": "~15.4.6",
16 | "expo-linking": "~6.2.2",
17 | "expo-router": "~3.4.8",
18 | "expo-status-bar": "~1.11.1",
19 | "react": "18.2.0",
20 | "react-native": "0.73.6",
21 | "react-native-gesture-handler": "~2.14.0",
22 | "react-native-reanimated": "~3.6.2",
23 | "react-native-safe-area-context": "4.8.2",
24 | "react-native-screens": "~3.29.0",
25 | "react-native-webview": "^13.8.6",
26 | "typescript": "^5.3.0"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "^7.20.0",
30 | "@types/chess.js": "^0.11.2"
31 | },
32 | "private": true
33 | }
34 |
--------------------------------------------------------------------------------
/apps/native/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {},
3 | "extends": "expo/tsconfig.base"
4 | }
5 |
--------------------------------------------------------------------------------
/apps/ws/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .env
4 |
--------------------------------------------------------------------------------
/apps/ws/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend1",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 |
8 | "dev": "esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.js --sourcemap && node dist/index.js",
9 | "lint": "eslint . --max-warnings 0",
10 | "lint:fix": "eslint --fix ."
11 |
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@prisma/client": "^5.12.1",
18 | "@repo/db": "*",
19 | "@types/ws": "^8.5.10",
20 | "chess.js": "^1.0.0-beta.8",
21 | "jsonwebtoken": "^9.0.2",
22 | "ws": "^8.16.0"
23 | },
24 | "devDependencies": {
25 | "@types/node": "^20.12.7",
26 | "nodemon": "^3.1.0",
27 | "prisma": "^5.12.1",
28 | "ts-node": "^10.9.2",
29 | "typescript": "^5.4.5"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/apps/ws/src/GameManager.ts:
--------------------------------------------------------------------------------
1 | import { WebSocket } from 'ws';
2 | import {
3 | GAME_OVER,
4 | INIT_GAME,
5 | JOIN_GAME,
6 | MOVE,
7 | OPPONENT_DISCONNECTED,
8 | JOIN_ROOM,
9 | GAME_JOINED,
10 | GAME_NOT_FOUND,
11 | GAME_ALERT,
12 | GAME_ADDED,
13 | GAME_ENDED,
14 | EXIT_GAME,
15 | } from './messages';
16 | import { Game, isPromoting } from './Game';
17 | import { db } from './db';
18 | import { socketManager, User } from './SocketManager';
19 | import { Square } from 'chess.js';
20 | import { GameStatus } from '@prisma/client';
21 |
22 | export class GameManager {
23 | private games: Game[];
24 | private pendingGameId: string | null;
25 | private users: User[];
26 |
27 | constructor() {
28 | this.games = [];
29 | this.pendingGameId = null;
30 | this.users = [];
31 | }
32 |
33 | addUser(user: User) {
34 | this.users.push(user);
35 | this.addHandler(user);
36 | }
37 |
38 | removeUser(socket: WebSocket) {
39 | const user = this.users.find((user) => user.socket === socket);
40 | if (!user) {
41 | console.error('User not found?');
42 | return;
43 | }
44 | this.users = this.users.filter((user) => user.socket !== socket);
45 | socketManager.removeUser(user);
46 | }
47 |
48 | removeGame(gameId: string) {
49 | this.games = this.games.filter((g) => g.gameId !== gameId);
50 | }
51 |
52 | private addHandler(user: User) {
53 | user.socket.on('message', async (data) => {
54 | const message = JSON.parse(data.toString());
55 | if (message.type === INIT_GAME) {
56 | if (this.pendingGameId) {
57 | const game = this.games.find((x) => x.gameId === this.pendingGameId);
58 | if (!game) {
59 | console.error('Pending game not found?');
60 | return;
61 | }
62 | if (user.userId === game.player1UserId) {
63 | socketManager.broadcast(
64 | game.gameId,
65 | JSON.stringify({
66 | type: GAME_ALERT,
67 | payload: {
68 | message: 'Trying to Connect with yourself?',
69 | },
70 | }),
71 | );
72 | return;
73 | }
74 | socketManager.addUser(user, game.gameId);
75 | await game?.updateSecondPlayer(user.userId);
76 | this.pendingGameId = null;
77 | } else {
78 | const game = new Game(user.userId, null);
79 | this.games.push(game);
80 | this.pendingGameId = game.gameId;
81 | socketManager.addUser(user, game.gameId);
82 | socketManager.broadcast(
83 | game.gameId,
84 | JSON.stringify({
85 | type: GAME_ADDED,
86 | gameId:game.gameId,
87 | }),
88 | );
89 | }
90 | }
91 |
92 | if (message.type === MOVE) {
93 | const gameId = message.payload.gameId;
94 | const game = this.games.find((game) => game.gameId === gameId);
95 | if (game) {
96 | game.makeMove(user, message.payload.move);
97 | if (game.result) {
98 | this.removeGame(game.gameId);
99 | }
100 | }
101 | }
102 |
103 | if (message.type === EXIT_GAME){
104 | const gameId = message.payload.gameId;
105 | const game = this.games.find((game) => game.gameId === gameId);
106 |
107 | if (game) {
108 | game.exitGame(user);
109 | this.removeGame(game.gameId)
110 | }
111 | }
112 |
113 | if (message.type === JOIN_ROOM) {
114 | const gameId = message.payload?.gameId;
115 | if (!gameId) {
116 | return;
117 | }
118 |
119 | let availableGame = this.games.find((game) => game.gameId === gameId);
120 | const gameFromDb = await db.game.findUnique({
121 | where: { id: gameId },
122 | include: {
123 | moves: {
124 | orderBy: {
125 | moveNumber: 'asc',
126 | },
127 | },
128 | blackPlayer: true,
129 | whitePlayer: true,
130 | },
131 | });
132 |
133 | // There is a game created but no second player available
134 |
135 | if (availableGame && !availableGame.player2UserId) {
136 | socketManager.addUser(user, availableGame.gameId);
137 | await availableGame.updateSecondPlayer(user.userId);
138 | return;
139 | }
140 |
141 | if (!gameFromDb) {
142 | user.socket.send(
143 | JSON.stringify({
144 | type: GAME_NOT_FOUND,
145 | }),
146 | );
147 | return;
148 | }
149 |
150 | if(gameFromDb.status !== GameStatus.IN_PROGRESS) {
151 | user.socket.send(JSON.stringify({
152 | type: GAME_ENDED,
153 | payload: {
154 | result: gameFromDb.result,
155 | status: gameFromDb.status,
156 | moves: gameFromDb.moves,
157 | blackPlayer: {
158 | id: gameFromDb.blackPlayer.id,
159 | name: gameFromDb.blackPlayer.name,
160 | },
161 | whitePlayer: {
162 | id: gameFromDb.whitePlayer.id,
163 | name: gameFromDb.whitePlayer.name,
164 | },
165 | }
166 | }));
167 | return;
168 | }
169 |
170 | if (!availableGame) {
171 | const game = new Game(
172 | gameFromDb?.whitePlayerId!,
173 | gameFromDb?.blackPlayerId!,
174 | gameFromDb.id,
175 | gameFromDb.startAt,
176 | );
177 | game.seedMoves(gameFromDb?.moves || []);
178 | this.games.push(game);
179 | availableGame = game;
180 | }
181 |
182 | console.log(availableGame.getPlayer1TimeConsumed());
183 | console.log(availableGame.getPlayer2TimeConsumed());
184 |
185 | user.socket.send(
186 | JSON.stringify({
187 | type: GAME_JOINED,
188 | payload: {
189 | gameId,
190 | moves: gameFromDb.moves,
191 | blackPlayer: {
192 | id: gameFromDb.blackPlayer.id,
193 | name: gameFromDb.blackPlayer.name,
194 | },
195 | whitePlayer: {
196 | id: gameFromDb.whitePlayer.id,
197 | name: gameFromDb.whitePlayer.name,
198 | },
199 | player1TimeConsumed: availableGame.getPlayer1TimeConsumed(),
200 | player2TimeConsumed: availableGame.getPlayer2TimeConsumed(),
201 | },
202 | }),
203 | );
204 |
205 | socketManager.addUser(user, gameId);
206 | }
207 | });
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/apps/ws/src/SocketManager.ts:
--------------------------------------------------------------------------------
1 | import { randomUUID } from 'crypto';
2 | import { WebSocket } from 'ws';
3 | import { userJwtClaims } from './auth';
4 |
5 | export class User {
6 | public socket: WebSocket;
7 | public id: string;
8 | public userId: string;
9 | public name: string;
10 | public isGuest?: boolean;
11 |
12 | constructor(socket: WebSocket, userJwtClaims: userJwtClaims) {
13 | this.socket = socket;
14 | this.userId = userJwtClaims.userId;
15 | this.id = randomUUID();
16 | this.name = userJwtClaims.name;
17 | this.isGuest = userJwtClaims.isGuest;
18 | }
19 | }
20 |
21 | class SocketManager {
22 | private static instance: SocketManager;
23 | private interestedSockets: Map;
24 | private userRoomMappping: Map;
25 |
26 | private constructor() {
27 | this.interestedSockets = new Map();
28 | this.userRoomMappping = new Map();
29 | }
30 |
31 | static getInstance() {
32 | if (SocketManager.instance) {
33 | return SocketManager.instance;
34 | }
35 |
36 | SocketManager.instance = new SocketManager();
37 | return SocketManager.instance;
38 | }
39 |
40 | addUser(user: User, roomId: string) {
41 | this.interestedSockets.set(roomId, [
42 | ...(this.interestedSockets.get(roomId) || []),
43 | user,
44 | ]);
45 | this.userRoomMappping.set(user.userId, roomId);
46 | }
47 |
48 | broadcast(roomId: string, message: string) {
49 | const users = this.interestedSockets.get(roomId);
50 | if (!users) {
51 | console.error('No users in room?');
52 | return;
53 | }
54 |
55 | users.forEach((user) => {
56 | user.socket.send(message);
57 | });
58 | }
59 |
60 | removeUser(user: User) {
61 | const roomId = this.userRoomMappping.get(user.userId);
62 | if (!roomId) {
63 | console.error('User was not interested in any room?');
64 | return;
65 | }
66 | const room = this.interestedSockets.get(roomId) || []
67 | const remainingUsers = room.filter(u =>
68 | u.userId !== user.userId
69 | )
70 | this.interestedSockets.set(
71 | roomId,
72 | remainingUsers
73 | );
74 | if (this.interestedSockets.get(roomId)?.length === 0) {
75 | this.interestedSockets.delete(roomId);
76 | }
77 | this.userRoomMappping.delete(user.userId);
78 | }
79 | }
80 |
81 | export const socketManager = SocketManager.getInstance()
82 |
--------------------------------------------------------------------------------
/apps/ws/src/auth/index.ts:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import { User } from '../SocketManager';
3 | import { Player } from '../Game';
4 | import { WebSocket } from 'ws';
5 |
6 | const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key';
7 |
8 | export interface userJwtClaims {
9 | userId: string;
10 | name: string;
11 | isGuest?: boolean;
12 | }
13 |
14 | export const extractAuthUser = (token: string, ws: WebSocket): User => {
15 | const decoded = jwt.verify(token, JWT_SECRET) as userJwtClaims;
16 | return new User(ws, decoded);
17 | };
18 |
--------------------------------------------------------------------------------
/apps/ws/src/db/index.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const client = new PrismaClient();
4 |
5 | export const db = client;
6 |
--------------------------------------------------------------------------------
/apps/ws/src/index.ts:
--------------------------------------------------------------------------------
1 | import { WebSocketServer } from 'ws';
2 | import { GameManager } from './GameManager';
3 | import url from 'url';
4 | import { extractAuthUser } from './auth';
5 |
6 | const wss = new WebSocketServer({ port: 8080 });
7 |
8 | const gameManager = new GameManager();
9 |
10 | wss.on('connection', function connection(ws, req) {
11 | //@ts-ignore
12 | const token: string = url.parse(req.url, true).query.token;
13 | const user = extractAuthUser(token, ws);
14 | gameManager.addUser(user);
15 |
16 | ws.on('close', () => {
17 | gameManager.removeUser(ws);
18 | });
19 | });
20 |
21 | console.log('done');
22 |
--------------------------------------------------------------------------------
/apps/ws/src/messages.ts:
--------------------------------------------------------------------------------
1 | export const INIT_GAME = 'init_game';
2 | export const MOVE = 'move';
3 | export const GAME_OVER = 'game_over';
4 | export const JOIN_GAME = 'join_game';
5 | export const OPPONENT_DISCONNECTED = 'opponent_disconnected';
6 | export const JOIN_ROOM = 'join_room';
7 | export const GAME_NOT_FOUND = 'game_not_found';
8 | export const GAME_JOINED = 'game_joined';
9 | export const GAME_ENDED = 'game_ended';
10 | export const GAME_ALERT = 'game_alert';
11 | export const GAME_ADDED = 'game_added';
12 | export const GAME_TIME = 'game_time';
13 | export const EXIT_GAME = 'exit_game';
14 |
--------------------------------------------------------------------------------
/apps/ws/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chess-monorepo",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "dev": "turbo dev",
7 | "lint": "turbo run lint",
8 | "lint:fix": "turbo lint:fix",
9 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
10 | "prepare": "husky"
11 | },
12 | "devDependencies": {
13 | "@repo/eslint-config": "*",
14 | "@repo/typescript-config": "*",
15 | "eslint-config-prettier": "^9.1.0",
16 | "eslint-plugin-prettier": "^5.1.3",
17 | "husky": "^9.0.11",
18 | "lint-staged": "^15.2.2",
19 | "prettier": "^3.2.5",
20 | "turbo": "latest"
21 | },
22 | "engines": {
23 | "node": ">=18"
24 | },
25 | "packageManager": "yarn@1.22.21",
26 | "workspaces": [
27 | "apps/*",
28 | "packages/*"
29 | ],
30 | "husky": {
31 | "hooks": {
32 | "pre-commit": "lint-staged"
33 | }
34 | },
35 | "lint-staged": {
36 | "**/*.{ts,tsx,md}": [
37 | "prettier --write",
38 | "eslint --fix"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/db/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres"
--------------------------------------------------------------------------------
/packages/db/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/db",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "db:generate":"prisma generate",
8 | "db:push": "prisma db push --skip-generate",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "db:dev": "npx prisma migrate dev && npx prisma generate",
11 | "db:seed": "prisma db seed"
12 | },
13 | "prisma": {
14 | "seed": "ts-node prisma/seed.ts"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "@prisma/client": "5.12.1",
21 | "prisma": "^5.12.0"
22 | },
23 | "exports": {
24 | "./client": "./src/index.ts"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240419000456_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "GameStatus" AS ENUM ('IN_PROGRESS', 'COMPLETED', 'ABANDONED');
3 |
4 | -- CreateEnum
5 | CREATE TYPE "GameResult" AS ENUM ('WHITE_WINS', 'BLACK_WINS', 'DRAW');
6 |
7 | -- CreateEnum
8 | CREATE TYPE "TimeControl" AS ENUM ('CLASSICAL', 'RAPID', 'BLITZ', 'BULLET');
9 |
10 | -- CreateTable
11 | CREATE TABLE "User" (
12 | "id" TEXT NOT NULL,
13 | "username" TEXT NOT NULL,
14 | "email" TEXT NOT NULL,
15 | "password" TEXT NOT NULL,
16 | "rating" INTEGER NOT NULL DEFAULT 1200,
17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
18 | "lastLogin" TIMESTAMP(3),
19 |
20 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
21 | );
22 |
23 | -- CreateTable
24 | CREATE TABLE "Game" (
25 | "id" TEXT NOT NULL,
26 | "whitePlayerId" TEXT NOT NULL,
27 | "blackPlayerId" TEXT NOT NULL,
28 | "status" "GameStatus" NOT NULL,
29 | "result" "GameResult",
30 | "timeControl" "TimeControl" NOT NULL,
31 | "startingFen" TEXT NOT NULL DEFAULT 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
32 | "currentFen" TEXT,
33 | "startAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
34 | "endAt" TIMESTAMP(3),
35 | "opening" TEXT,
36 | "event" TEXT,
37 |
38 | CONSTRAINT "Game_pkey" PRIMARY KEY ("id")
39 | );
40 |
41 | -- CreateTable
42 | CREATE TABLE "Move" (
43 | "id" TEXT NOT NULL,
44 | "gameId" TEXT NOT NULL,
45 | "moveNumber" INTEGER NOT NULL,
46 | "notation" TEXT NOT NULL,
47 | "comments" TEXT,
48 | "startFen" TEXT NOT NULL,
49 | "endFen" TEXT NOT NULL,
50 | "timeTaken" INTEGER DEFAULT 0,
51 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
52 |
53 | CONSTRAINT "Move_pkey" PRIMARY KEY ("id")
54 | );
55 |
56 | -- CreateIndex
57 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
58 |
59 | -- CreateIndex
60 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
61 |
62 | -- CreateIndex
63 | CREATE INDEX "User_rating_idx" ON "User"("rating");
64 |
65 | -- CreateIndex
66 | CREATE INDEX "Game_status_result_idx" ON "Game"("status", "result");
67 |
68 | -- CreateIndex
69 | CREATE INDEX "Move_gameId_idx" ON "Move"("gameId");
70 |
71 | -- AddForeignKey
72 | ALTER TABLE "Game" ADD CONSTRAINT "Game_whitePlayerId_fkey" FOREIGN KEY ("whitePlayerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
73 |
74 | -- AddForeignKey
75 | ALTER TABLE "Game" ADD CONSTRAINT "Game_blackPlayerId_fkey" FOREIGN KEY ("blackPlayerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
76 |
77 | -- AddForeignKey
78 | ALTER TABLE "Move" ADD CONSTRAINT "Move_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
79 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240419114530_update_schema/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `provider` to the `User` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- CreateEnum
8 | CREATE TYPE "AuthProvider" AS ENUM ('EMAIL', 'GOOGLE', 'FACEBOOK', 'GITHUB');
9 |
10 | -- AlterTable
11 | ALTER TABLE "User" ADD COLUMN "provider" "AuthProvider" NOT NULL,
12 | ALTER COLUMN "username" DROP NOT NULL,
13 | ALTER COLUMN "password" DROP NOT NULL;
14 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240419114834_add_name/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "name" TEXT;
3 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240419130914_add_from_to/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `notation` on the `Move` table. All the data in the column will be lost.
5 | - Added the required column `from` to the `Move` table without a default value. This is not possible if the table is not empty.
6 | - Added the required column `to` to the `Move` table without a default value. This is not possible if the table is not empty.
7 |
8 | */
9 | -- AlterTable
10 | ALTER TABLE "Move" DROP COLUMN "notation",
11 | ADD COLUMN "from" TEXT NOT NULL,
12 | ADD COLUMN "to" TEXT NOT NULL;
13 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240420233109_add_guest_auth_provider/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "AuthProvider" ADD VALUE 'GUEST';
3 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240422001721_add_time_up/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "GameStatus" ADD VALUE 'TIME_UP';
3 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240422115020_add_san_to_move/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Move" ADD COLUMN "san" TEXT;
3 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240422122937_rename_columns_in_moves_table/migration.sql:
--------------------------------------------------------------------------------
1 |
2 | -- AlterTable
3 | ALTER TABLE "Move" RENAME COLUMN "endFen" to "after";
4 | ALTER TABLE "Move" RENAME COLUMN "startFen" to "before";
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240508152337_remove_facebook_login/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - The values [FACEBOOK] on the enum `AuthProvider` will be removed. If these variants are still used in the database, this will fail.
5 |
6 | */
7 | -- AlterEnum
8 | BEGIN;
9 | CREATE TYPE "AuthProvider_new" AS ENUM ('EMAIL', 'GOOGLE', 'GITHUB');
10 | ALTER TABLE "User" ALTER COLUMN "provider" TYPE "AuthProvider_new" USING ("provider"::text::"AuthProvider_new");
11 | ALTER TYPE "AuthProvider" RENAME TO "AuthProvider_old";
12 | ALTER TYPE "AuthProvider_new" RENAME TO "AuthProvider";
13 | DROP TYPE "AuthProvider_old";
14 | COMMIT;
15 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240618172755_add_player_exit/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "GameStatus" ADD VALUE 'PLAYER_EXIT';
3 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/packages/db/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(uuid())
12 | username String? @unique
13 | name String?
14 | email String @unique
15 | provider AuthProvider
16 | password String?
17 | rating Int @default(1200)
18 | gamesAsWhite Game[] @relation("GamesAsWhite")
19 | gamesAsBlack Game[] @relation("GamesAsBlack")
20 | createdAt DateTime @default(now())
21 | lastLogin DateTime?
22 |
23 | @@index([rating])
24 | }
25 |
26 | model Game {
27 | id String @id @default(uuid())
28 | whitePlayerId String
29 | blackPlayerId String
30 | whitePlayer User @relation("GamesAsWhite", fields: [whitePlayerId], references: [id])
31 | blackPlayer User @relation("GamesAsBlack", fields: [blackPlayerId], references: [id])
32 | status GameStatus
33 | result GameResult?
34 | timeControl TimeControl
35 | startingFen String @default("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") // default value for the startingFen
36 | currentFen String?
37 | startAt DateTime @default(now())
38 | endAt DateTime?
39 | moves Move[]
40 | opening String?
41 | event String?
42 |
43 | @@index([status, result])
44 | }
45 |
46 | model Move {
47 | id String @id @default(uuid())
48 | gameId String
49 | game Game @relation(fields: [gameId], references: [id])
50 | moveNumber Int
51 | from String
52 | to String
53 | comments String?
54 | before String
55 | after String
56 | timeTaken Int? @default(0)
57 | createdAt DateTime @default(now())
58 | san String?
59 |
60 | @@index([gameId])
61 | }
62 |
63 | enum GameStatus {
64 | IN_PROGRESS
65 | COMPLETED
66 | ABANDONED
67 | TIME_UP
68 | PLAYER_EXIT
69 | }
70 |
71 | enum GameResult {
72 | WHITE_WINS
73 | BLACK_WINS
74 | DRAW
75 | }
76 |
77 | enum TimeControl {
78 | CLASSICAL
79 | RAPID
80 | BLITZ
81 | BULLET
82 | }
83 |
84 | enum AuthProvider {
85 | EMAIL
86 | GOOGLE
87 | GITHUB
88 | GUEST
89 | }
--------------------------------------------------------------------------------
/packages/db/src/index.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | type PrismaClientSingleton = ReturnType;
8 |
9 | // eslint-disable-next-line
10 | const globalForPrisma = globalThis as unknown as {
11 | prisma: PrismaClientSingleton | undefined;
12 | };
13 |
14 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
15 |
16 | export default prisma;
17 |
18 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
19 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"],
8 | plugins: ["only-warn"],
9 | globals: {
10 | React: true,
11 | JSX: true,
12 | },
13 | env: {
14 | node: true,
15 | },
16 | settings: {
17 | "import/resolver": {
18 | typescript: {
19 | project,
20 | },
21 | },
22 | },
23 | ignorePatterns: [
24 | // Ignore dotfiles
25 | ".*.js",
26 | "node_modules/",
27 | "dist/",
28 | ],
29 | overrides: [
30 | {
31 | files: ["*.js?(x)", "*.ts?(x)"],
32 | },
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('node:path');
2 | const project = resolve(process.cwd(), 'tsconfig.json');
3 |
4 | /** @type {import("eslint").Linter.Config} */
5 | module.exports = {
6 | extends: [
7 | 'eslint:recommended',
8 | 'prettier',
9 | require.resolve('@vercel/style-guide/eslint/next'),
10 | 'eslint-config-turbo',
11 | ],
12 | globals: {
13 | React: true,
14 | JSX: true,
15 | },
16 | env: {
17 | node: true,
18 | browser: true,
19 | },
20 | plugins: ['only-warn'],
21 | settings: {
22 | 'import/resolver': {
23 | typescript: { project },
24 | },
25 | },
26 | ignorePatterns: [
27 | // Ignore dotfiles
28 | '.*.js',
29 | 'node_modules/',
30 | ],
31 | overrides: [{ files: ['*.js?(x)', '*.ts?(x)'] }],
32 | };
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "library.js",
7 | "next.js",
8 | "react-internal.js"
9 | ],
10 | "devDependencies": {
11 | "@vercel/style-guide": "^5.2.0",
12 | "eslint-config-turbo": "^1.12.4",
13 | "eslint-config-prettier": "^9.1.0",
14 | "eslint-plugin-only-warn": "^1.1.0",
15 | "@typescript-eslint/parser": "^7.1.0",
16 | "@typescript-eslint/eslint-plugin": "^7.1.0",
17 | "typescript": "^5.3.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | *
10 | * This config extends the Vercel Engineering Style Guide.
11 | * For more information, see https://github.com/vercel/style-guide
12 | *
13 | */
14 |
15 | /** @type {import("eslint").Linter.Config} */
16 | module.exports = {
17 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"],
18 | plugins: ["only-warn"],
19 | globals: {
20 | React: true,
21 | JSX: true,
22 | },
23 | env: {
24 | browser: true,
25 | },
26 | settings: {
27 | "import/resolver": {
28 | typescript: {
29 | project,
30 | },
31 | },
32 | },
33 | ignorePatterns: [
34 | // Ignore dotfiles
35 | ".*.js",
36 | "node_modules/",
37 | "dist/",
38 | ],
39 | overrides: [
40 | // Force ESLint to detect .tsx files
41 | { files: ["*.js?(x)", "*.ts?(x)"] },
42 | ],
43 | };
44 |
--------------------------------------------------------------------------------
/packages/store/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/store",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "exports": {
7 | "./useUser": "./src/hooks/useUser.ts",
8 | "./userAtom": "./src/atoms/user.ts",
9 | "./chessBoard": "./src/atoms/chessBoard.ts"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "recoil": "^0.7.7"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/store/src/atoms/chessBoard.ts:
--------------------------------------------------------------------------------
1 | import { Move } from 'chess.js';
2 | import { atom } from "recoil";
3 |
4 | export const isBoardFlippedAtom = atom({
5 | key: "isBoardFlippedAtom",
6 | default: false,
7 | })
8 |
9 | export const movesAtom = atom({
10 | key: "movesAtom",
11 | default: []
12 | })
13 |
14 | export const userSelectedMoveIndexAtom = atom({
15 | key: 'userSelectedMoveIndex',
16 | default: null
17 | });
--------------------------------------------------------------------------------
/packages/store/src/atoms/user.ts:
--------------------------------------------------------------------------------
1 | import { atom, selector } from 'recoil';
2 |
3 | // How do you put this in .env? @hkirat
4 | export const BACKEND_URL = 'http://localhost:3000';
5 | export interface User {
6 | token: string;
7 | id: string;
8 | name: string;
9 | }
10 |
11 | export const userAtom = atom({
12 | key: 'user',
13 | default: selector({
14 | key: 'user/default',
15 | get: async () => {
16 | try {
17 | const response = await fetch(`${BACKEND_URL}/auth/refresh`, {
18 | method: 'GET',
19 | headers: {
20 | 'Content-Type': 'application/json',
21 | },
22 | credentials: 'include',
23 | });
24 | if (response.ok) {
25 | const data = await response.json();
26 | return data;
27 | }
28 | } catch (e) {
29 | console.error(e);
30 | }
31 |
32 | return null;
33 | },
34 | }),
35 | });
36 |
--------------------------------------------------------------------------------
/packages/store/src/hooks/useUser.ts:
--------------------------------------------------------------------------------
1 | import { useRecoilValue } from 'recoil';
2 | import { userAtom } from '../atoms/user';
3 |
4 | export const useUser = () => {
5 | const value = useRecoilValue(userAtom);
6 | return value;
7 | };
8 |
--------------------------------------------------------------------------------
/packages/tailwind-Config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/tailwind-config",
3 | "version": "0.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "files": [
7 | "postcss.config.js",
8 | "tailwind.config.js"
9 | ]
10 | }
--------------------------------------------------------------------------------
/packages/tailwind-Config/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/packages/tailwind-Config/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "isolatedModules": true,
10 | "lib": ["es2022", "DOM", "DOM.Iterable"],
11 | "module": "NodeNext",
12 | "moduleDetection": "force",
13 | "moduleResolution": "NodeNext",
14 | "noUncheckedIndexedAccess": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2022"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "allowJs": true,
10 | "jsx": "preserve",
11 | "noEmit": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/react-internal.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: "./tsconfig.lint.json",
8 | tsconfigRootDir: __dirname,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/ui",
3 | "version": "0.0.0",
4 | "private": true,
5 | "exports": {
6 | "./button": "./src/button.tsx",
7 | "./card": "./src/card.tsx",
8 | "./code": "./src/code.tsx"
9 | },
10 | "scripts": {
11 | "lint": "eslint . --max-warnings 0",
12 | "generate:component": "turbo gen react-component"
13 | },
14 | "devDependencies": {
15 | "@repo/eslint-config": "*",
16 | "@repo/typescript-config": "*",
17 | "@turbo/gen": "^1.12.4",
18 | "@types/eslint": "^8.56.5",
19 | "@types/node": "^20.11.24",
20 | "@types/react": "^18.2.61",
21 | "@types/react-dom": "^18.2.19",
22 | "eslint": "^8.57.0",
23 | "react": "^18.2.0",
24 | "typescript": "^5.3.3"
25 | },
26 | "dependencies": {
27 | "lucide-react": "^0.372.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/ui/src/button.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ReactNode } from 'react';
4 |
5 | interface ButtonProps {
6 | children: ReactNode;
7 | className?: string;
8 | appName: string;
9 | }
10 |
11 | export const Button = ({ children, className, appName }: ButtonProps) => {
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/packages/ui/src/card.tsx:
--------------------------------------------------------------------------------
1 | export function Card({
2 | className,
3 | title,
4 | children,
5 | href,
6 | }: {
7 | className?: string;
8 | title: string;
9 | children: React.ReactNode;
10 | href: string;
11 | }): JSX.Element {
12 | return (
13 |
19 |
20 | {title} ->
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/ui/src/code.tsx:
--------------------------------------------------------------------------------
1 | export function Code({
2 | children,
3 | className,
4 | }: {
5 | children: React.ReactNode;
6 | className?: string;
7 | }): JSX.Element {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.lint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src", "turbo"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import type { PlopTypes } from '@turbo/gen';
2 |
3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
4 |
5 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
6 | // A simple generator to add a new React component to the internal UI library
7 | plop.setGenerator('react-component', {
8 | description: 'Adds a new react component',
9 | prompts: [
10 | {
11 | type: 'input',
12 | name: 'name',
13 | message: 'What is the name of the component?',
14 | },
15 | ],
16 | actions: [
17 | {
18 | type: 'add',
19 | path: 'src/{{kebabCase name}}.tsx',
20 | templateFile: 'templates/component.hbs',
21 | },
22 | {
23 | type: 'append',
24 | path: 'package.json',
25 | pattern: /"exports": {(?)/g,
26 | template: '"./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
27 | },
28 | ],
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/templates/component.hbs:
--------------------------------------------------------------------------------
1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 |
4 |
{{ pascalCase name }} Component
5 | {children}
6 |
7 | );
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/base.json"
3 |
4 | }
5 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": [
4 | "**/.env.*local",
5 | "COOKIE_SECRET",
6 | "ALLOWED_HOSTS",
7 | "PORT",
8 | "GOOGLE_CLIENT_ID",
9 | "GOOGLE_CLIENT_SECRET",
10 | "GITHUB_CLIENT_ID",
11 | "GITHUB_CLIENT_SECRET",
12 | "FACEBOOK_APP_ID",
13 | "FACEBOOK_APP_SECRET",
14 | "AUTH_REDIRECT_URL",
15 | "JWT_SECRET"
16 | ],
17 | "pipeline": {
18 | "build": {
19 | "dependsOn": ["db:generate", "^build"],
20 | "outputs": [".next/**", "!.next/cache/**"]
21 | },
22 | "lint": {
23 | "dependsOn": ["^lint"]
24 | },
25 | "lint:fix": {
26 | "dependsOn": ["^lint:fix"]
27 | },
28 | "dev": {
29 | "dependsOn": ["^db:generate"],
30 | "cache": false,
31 | "persistent": true
32 | },
33 | "db:generate": {
34 | "cache": false
35 | },
36 | "db:push": {
37 | "cache": false
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------