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 ( + + ); + })} {/* --- Modal and Info (Unchanged) --- */} {isModalOpen && (

Join a Room

+ setUsername(e.target.value)} + className="border border-gray-400 rounded-md px-4 py-2 w-64 text-center" + /> { + console.log("πŸ–ŒοΈ Cursor component rendering:", { x, y, username, color }); + + return ( +
+ {/* Simple Mouse Cursor SVG */} + + + + + {/* Username label */} +
+ {username} +
+
+ ); +}; diff --git a/server/index.js b/server/index.js index 5cba415..b1a7ea5 100644 --- a/server/index.js +++ b/server/index.js @@ -32,23 +32,86 @@ const io = new Server(server, { }, }); +// Track users in rooms +const roomUsers = new Map(); // roomId -> Set of { socketId, username } + io.on("connection", (socket) => { console.log("🟒 User connected:", socket.id); // Join room - socket.on("join-room", (roomId) => { + socket.on("join-room", (roomId, username) => { socket.join(roomId); + + // Add user to room tracking + if (!roomUsers.has(roomId)) { + roomUsers.set(roomId, new Map()); + } + roomUsers.get(roomId).set(socket.id, { username: username || `User-${socket.id.slice(0, 4)}` }); + console.log(`User ${socket.id} joined room ${roomId}`); + + // Notify others in the room about the new user + socket.to(roomId).emit("user-joined", { + userId: socket.id, + username: username || `User-${socket.id.slice(0, 4)}` + }); + + // Send existing users list to the new user + const existingUsers = Array.from(roomUsers.get(roomId).entries()) + .filter(([id]) => id !== socket.id) + .map(([id, data]) => ({ userId: id, username: data.username })); + socket.emit("existing-users", existingUsers); }); // Forward drawing events to all others in the room socket.on("draw", (data) => { const { roomId } = data; + console.log(`🎨 Draw event from ${socket.id} in room ${roomId}`); socket.to(roomId).emit("draw", data); }); + + // Handle cursor position updates + socket.on("cursor-move", (data) => { + const { roomId, x, y } = data; + console.log(`πŸ–±οΈ Cursor from ${socket.id} in room ${roomId}:`, x, y); + socket.to(roomId).emit("cursor-move", { + userId: socket.id, + x, + y + }); + }); + + // Handle leave room + socket.on("leave-room", (roomId) => { + socket.leave(roomId); + + // Remove user from room tracking + if (roomUsers.has(roomId)) { + roomUsers.get(roomId).delete(socket.id); + if (roomUsers.get(roomId).size === 0) { + roomUsers.delete(roomId); + } + } + + // Notify others in the room + socket.to(roomId).emit("user-left", { userId: socket.id }); + console.log(`User ${socket.id} left room ${roomId}`); + }); socket.on("disconnect", () => { console.log("πŸ”΄ User disconnected:", socket.id); + + // Remove user from all rooms and notify others + roomUsers.forEach((users, roomId) => { + if (users.has(socket.id)) { + users.delete(socket.id); + socket.to(roomId).emit("user-left", { userId: socket.id }); + + if (users.size === 0) { + roomUsers.delete(roomId); + } + } + }); }); });