** How to Run Locally **
This project uses pnpm so you need to install it first.
- Clone the repo
- Run
pnpm install - Start the DB locally using Docker or get from Neon or Supabase
- Create a .env file in db folder and put database connection string
- Migrate the Database using
npx prisma migrate dev - Generate the Client using
npx prisma generate - Start http backend
- Start WS backend
- Start Frontend
Notes:
How to extend tsconfig.json
{ "extends" : "@repo/typescript-config/base.json" }
Add this to package.json
"devDependencies" : { "workspace": "*" }
-
Initialize a http-server && a websocket server
-
Add Signup && Login && Create Room endpoint in express
-
Setup ws and gatekeep using jwt
-
Adding Supabase
-
Create DB and Common package for Zod Schema and JWT_secret
-
Use Prisma
-
pnpm prisma install,
-
npx primsa init => Add Schema,
-
npx prisma migrate dev --name init_schema
-
npx prisma generate -> Generates a client
-
ws layer, room management and broadcast messages
-
Adding ws logic on the backend
-
State Management on the backend => Stateful Socket Server
-
Same User Can Join Multiple Rooms
-
Approach1: Global Variable
-
For Signup && Signin look similar, a single component can be made AuthPage.tsx, and it can accept a props based on which we can see singup or signin UI.
-
How to implement the mouse hold detection - I used combination of onMouseDown, onMouseUp, and onMouseMove (there are better ways to do this.)
-
Migrating code to logic/game file
-
We seperate the logic into RoomCanvas and Canvas component to avoid the race conditions.(canvas being rendered before webSocket connection is made)
-
npx prisma studioto inspect database locally -
used custom events to pass selected shape
-
More features
-
Add Signup and Signin page
-
Adding dashboard to show all the rooms created by user
-
Adding extra shapes, and text
-
Adding eraser(delete using id of message?)
-
Adding image feature
-
Allow to undo/redo
-
Allow moving shapes
-
Add delete room button
-
Allow resizing canvas
-
Add freehand drawing (Challenging)
-
Add panning and zooming (Challenging)
-
Tricky Bits
useEffect(() => {
if(!canvasRef || !canvasRef.current){
return;
}
alert('new instance created')
const b = new Board(canvasRef.current, roomId, socket);
setBoard(b);
return () => {
b.destroy();
}
}, [canvasRef])
useEffect(() => {
if (!board || !selectedShape) return;
board.setSelectedShapeType(selectedShape);
}, [selectedShape, board]);When useEffect runs twice in strict mode.
Then it creates two instances of board.
We lose access to first one but it is still there in memory with event listeners attached to it and using default shape 'rect'.
The second one is the one which we have access over and we update the shape it uses
So, when we draw we get two shapes because of two instances.
Solution, is to remove event listener attached to old instance as a cleanup.
- Component Unmount: When component is removed from DOM
- Dependency Changes: Before useEffect runs again due to dependency updates
- StrictMode Development: During the double-mounting process:
// First mount const b1 = new Board(...); // Create first instance // StrictMode triggers unmount b1.destroy(); // Cleanup runs, removing b1's listeners // Second mount const b2 = new Board(...); // Create second instance
-
Route Change:
- User navigates away = cleanup runs
- Old Board instance destroyed
-
Dependency Updates:
useEffect(() => { const b = new Board(canvasRef.current, roomId, socket); return () => b.destroy(); }, [roomId]) // If roomId changes: // 1. Cleanup runs (old instance destroyed) // 2. New instance created with new roomId
-
Component Re-renders from Prop/State Changes:
- If dependencies change = cleanup runs
- New instance created with updated values