diff --git a/client/src/components/Canvas.jsx b/client/src/components/Canvas.jsx
index 52679fb..a620bc5 100644
--- a/client/src/components/Canvas.jsx
+++ b/client/src/components/Canvas.jsx
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { Toolbar } from "./Toolbar";
import { ColorPicker } from "./ColorPicker";
import { StrokeControl } from "./StrokeControl";
+import { Cursor } from "./Cursor";
import { toast } from "sonner";
import { io } from "socket.io-client";
import tinycolor from "tinycolor2";
@@ -28,10 +29,15 @@ export const Canvas = () => {
// --- Collaboration State ---
const [roomId, setRoomId] = useState("");
+ const [username, setUsername] = useState("");
const [joined, setJoined] = useState(false);
const [socket, setSocket] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
+ // --- Cursor Tracking State ---
+ const [otherCursors, setOtherCursors] = useState(new Map()); // userId -> { x, y, username, color }
+ const cursorColors = useRef(new Map()); // userId -> color
+
const [isLoggedIn, setIsLoggedIn] = useState(!!localStorage.getItem("token"));
const handleLogout = async () => {
@@ -115,12 +121,34 @@ export const Canvas = () => {
canvasImage.current = canvas.toDataURL();
};
+ // Generate a random color for a user
+ const getColorForUser = (userId) => {
+ if (!cursorColors.current.has(userId)) {
+ const colors = [
+ "#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA07A",
+ "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E2",
+ "#F8B739", "#52D3AA", "#E74C3C", "#3498DB"
+ ];
+ const color = colors[Math.floor(Math.random() * colors.length)];
+ cursorColors.current.set(userId, color);
+ }
+ return cursorColors.current.get(userId);
+ };
+
useEffect(() => {
const s = io("http://localhost:3000");
setSocket(s);
- s.on("connect", () => console.log("Connected to server:", s.id));
+
+ s.on("connect", () => {
+ console.log("β
Connected to server:", s.id);
+ });
+
+ s.on("disconnect", () => {
+ console.log("β Disconnected from server");
+ });
+
s.on("draw", ({ x, y, color, width, type, tool }) => {
- if (!joined) return;
+ console.log("π₯ Received draw event:", { x, y, type, tool });
const ctx = canvasRef.current?.getContext("2d");
if (!ctx) return;
@@ -139,8 +167,80 @@ export const Canvas = () => {
ctx.restore(); // Restore to default transform
saveCanvasState(); // Save state after remote draw
});
- return () => s.disconnect();
- }, [joined, scale, offset]); // Add scale/offset dependencies
+
+ // Handle cursor movements from other users
+ s.on("cursor-move", ({ userId, x, y }) => {
+ console.log("π±οΈ Received cursor from:", userId, "at", x, y);
+ setOtherCursors((prev) => {
+ const updated = new Map(prev);
+ const existing = updated.get(userId) || {};
+ const newCursor = {
+ x,
+ y,
+ username: existing.username || `User-${userId.slice(0, 4)}`,
+ color: getColorForUser(userId)
+ };
+ console.log("π Setting cursor:", userId, newCursor);
+ updated.set(userId, newCursor);
+ console.log("πΊοΈ Total cursors:", updated.size);
+ return updated;
+ });
+ });
+
+ // Handle new user joining
+ s.on("user-joined", ({ userId, username }) => {
+ console.log("π€ User joined:", username, "(ID:", userId, ")");
+ setOtherCursors((prev) => {
+ const updated = new Map(prev);
+ updated.set(userId, {
+ x: 0,
+ y: 0,
+ username,
+ color: getColorForUser(userId)
+ });
+ console.log("πΊοΈ Total cursors after join:", updated.size);
+ return updated;
+ });
+ toast.info(`${username} joined the room`);
+ });
+
+ // Handle existing users when joining
+ s.on("existing-users", (users) => {
+ console.log("π₯ Existing users:", users);
+ setOtherCursors((prev) => {
+ const updated = new Map(prev);
+ users.forEach(({ userId, username }) => {
+ updated.set(userId, {
+ x: 0,
+ y: 0,
+ username,
+ color: getColorForUser(userId)
+ });
+ });
+ return updated;
+ });
+ });
+
+ // Handle user leaving
+ s.on("user-left", ({ userId }) => {
+ console.log("π User left:", userId);
+ setOtherCursors((prev) => {
+ const updated = new Map(prev);
+ const user = updated.get(userId);
+ updated.delete(userId);
+ cursorColors.current.delete(userId);
+ if (user) {
+ toast.info(`${user.username} left the room`);
+ }
+ return updated;
+ });
+ });
+
+ return () => {
+ console.log("π Disconnecting socket...");
+ s.disconnect();
+ };
+ }, []); // Remove dependencies to prevent socket recreation!
useEffect(() => {
const canvas = canvasRef.current;
@@ -527,7 +627,9 @@ export const Canvas = () => {
// --- Collaboration Handlers (Unchanged) ---
const handleJoinRoom = () => {
if (!roomId.trim() || !socket) return;
- socket.emit("join-room", roomId.trim());
+ const displayName = username.trim() || `User-${socket.id?.slice(0, 4)}`;
+ console.log("π¨ Joining room:", roomId.trim(), "as", displayName);
+ socket.emit("join-room", roomId.trim(), displayName);
setJoined(true);
setIsModalOpen(false);
toast.success(`Collaborative mode active - joined room: ${roomId}`);
@@ -542,6 +644,8 @@ export const Canvas = () => {
if (socket) {
socket.emit("leave-room", roomId);
setJoined(false);
+ setOtherCursors(new Map()); // Clear all cursors
+ cursorColors.current.clear(); // Clear color mappings
toast.success(`Left room: ${roomId}`);
}
};
@@ -641,17 +745,45 @@ export const Canvas = () => {
onFocus={() => setIsCanvasFocused(true)}
onBlur={() => setIsCanvasFocused(false)}
onMouseDown={startDrawing}
- onMouseMove={draw}
+ onMouseMove={(e) => {
+ // Send cursor position to other users when in a room
+ if (joined && socket) {
+ const { x, y } = getWorldPoint(e);
+ socket.emit("cursor-move", { roomId, x, y });
+ }
+ draw(e);
+ }}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onWheel={handleWheel} // Added wheel handler
className={`${getCursor()} focus:outline-2 focus:outline-primary`} // Dynamic cursor
/>
+
+ {/* --- Render Other Users' Cursors --- */}
+ {joined && Array.from(otherCursors.entries()).map(([userId, cursor]) => {
+ console.log("π― Rendering cursor for:", userId, cursor);
+ return (
+