Created
March 4, 2025 11:44
-
-
Save shricodev/1a8171fdd875fb821fddfe88a76ba484 to your computer and use it in GitHub Desktop.
Collaborative whiteboard application (generated by Claude 3.7 Sonnet AI Model). This gist is for one of my blog post comparison of AI Models.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useEffect, useRef, useState } from "react"; | |
| import io, { Socket } from "socket.io-client"; | |
| interface Point { | |
| x: number; | |
| y: number; | |
| } | |
| interface DrawLine { | |
| prevPoint: Point | null; | |
| currentPoint: Point; | |
| color: string; | |
| } | |
| export default function WhiteboardPage() { | |
| const canvasRef = useRef<HTMLCanvasElement>(null); | |
| const [socket, setSocket] = useState<Socket | null>(null); | |
| const [isDrawing, setIsDrawing] = useState(false); | |
| const [color, setColor] = useState("#000000"); | |
| const [username, setUsername] = useState(""); | |
| const [isLoggedIn, setIsLoggedIn] = useState(false); | |
| const [onlineUsers, setOnlineUsers] = useState<string[]>([]); | |
| // Socket connection setup | |
| useEffect(() => { | |
| if (isLoggedIn) { | |
| const socketInstance = io( | |
| process.env.NEXT_PUBLIC_SOCKET_URL || "http://localhost:3001", | |
| ); | |
| socketInstance.on("connect", () => { | |
| console.log("Connected to socket server"); | |
| socketInstance.emit("user-join", username); | |
| }); | |
| socketInstance.on("update-users", (users: string[]) => { | |
| setOnlineUsers(users); | |
| }); | |
| socketInstance.on( | |
| "draw-line", | |
| ({ prevPoint, currentPoint, color }: DrawLine) => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) return; | |
| drawLine({ prevPoint, currentPoint, color }, ctx); | |
| }, | |
| ); | |
| setSocket(socketInstance); | |
| return () => { | |
| socketInstance.disconnect(); | |
| }; | |
| } | |
| }, [isLoggedIn, username]); | |
| // Canvas initialization | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) return; | |
| // Set canvas size | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight - 100; // Leave space for UI elements | |
| // Fill canvas with white background | |
| ctx.fillStyle = "white"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Handle window resize | |
| const handleResize = () => { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight - 100; | |
| ctx.fillStyle = "white"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.putImageData(imageData, 0, 0); | |
| }; | |
| window.addEventListener("resize", handleResize); | |
| return () => { | |
| window.removeEventListener("resize", handleResize); | |
| }; | |
| }, []); | |
| const drawLine = (line: DrawLine, context: CanvasRenderingContext2D) => { | |
| const { prevPoint, currentPoint, color } = line; | |
| const startPoint = prevPoint ?? currentPoint; | |
| context.beginPath(); | |
| context.lineWidth = 3; | |
| context.strokeStyle = color; | |
| context.moveTo(startPoint.x, startPoint.y); | |
| context.lineTo(currentPoint.x, currentPoint.y); | |
| context.lineCap = "round"; | |
| context.lineJoin = "round"; | |
| context.stroke(); | |
| }; | |
| // Canvas event handlers | |
| const onMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => { | |
| if (!isLoggedIn) return; | |
| setIsDrawing(true); | |
| const currentPoint = { | |
| x: e.nativeEvent.offsetX, | |
| y: e.nativeEvent.offsetY, | |
| }; | |
| const ctx = canvasRef.current?.getContext("2d"); | |
| if (ctx) { | |
| ctx.beginPath(); | |
| ctx.moveTo(currentPoint.x, currentPoint.y); | |
| ctx.lineWidth = 3; | |
| ctx.strokeStyle = color; | |
| ctx.lineCap = "round"; | |
| ctx.lineJoin = "round"; | |
| } | |
| }; | |
| const onMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => { | |
| if (!isDrawing || !socket || !isLoggedIn) return; | |
| const prevPoint = { | |
| x: e.nativeEvent.offsetX - e.nativeEvent.movementX, | |
| y: e.nativeEvent.offsetY - e.nativeEvent.movementY, | |
| }; | |
| const currentPoint = { | |
| x: e.nativeEvent.offsetX, | |
| y: e.nativeEvent.offsetY, | |
| }; | |
| // Draw on local canvas | |
| const ctx = canvasRef.current?.getContext("2d"); | |
| if (ctx) { | |
| drawLine({ prevPoint, currentPoint, color }, ctx); | |
| } | |
| // Send drawing data to server | |
| socket.emit("draw-line", { prevPoint, currentPoint, color }); | |
| }; | |
| const onMouseUp = () => { | |
| setIsDrawing(false); | |
| }; | |
| const clearCanvas = () => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) return; | |
| ctx.fillStyle = "white"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| if (socket) { | |
| socket.emit("clear-canvas"); | |
| } | |
| }; | |
| const handleLogin = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (username.trim()) { | |
| setIsLoggedIn(true); | |
| } | |
| }; | |
| return ( | |
| <div className="flex flex-col h-screen bg-gray-100 text-black"> | |
| {!isLoggedIn ? ( | |
| <div className="flex items-center justify-center h-full"> | |
| <form | |
| onSubmit={handleLogin} | |
| className="bg-white p-8 rounded-lg shadow-md w-80" | |
| > | |
| <h2 className="text-2xl font-bold mb-6 text-center text-gray-800"> | |
| Join Whiteboard | |
| </h2> | |
| <div className="mb-4"> | |
| <label htmlFor="username" className="block text-gray-700 mb-2"> | |
| Username | |
| </label> | |
| <input | |
| type="text" | |
| id="username" | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| required | |
| /> | |
| </div> | |
| <button | |
| type="submit" | |
| className="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 transition-colors" | |
| > | |
| Join | |
| </button> | |
| </form> | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="bg-white shadow-md p-4 flex justify-between items-center"> | |
| <div className="flex items-center space-x-4"> | |
| <h1 className="text-xl font-bold text-gray-800"> | |
| Collaborative Whiteboard | |
| </h1> | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-gray-700">Color:</span> | |
| <input | |
| type="color" | |
| value={color} | |
| onChange={(e) => setColor(e.target.value)} | |
| className="h-8 w-8 border border-gray-300 rounded" | |
| /> | |
| </div> | |
| <button | |
| onClick={clearCanvas} | |
| className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors" | |
| > | |
| Clear Canvas | |
| </button> | |
| </div> | |
| <div className="flex items-center"> | |
| <div className="mr-4"> | |
| <span className="font-semibold"> | |
| Online Users ({onlineUsers.length}): | |
| </span> | |
| <span className="ml-2">{onlineUsers.join(", ")}</span> | |
| </div> | |
| <div className="bg-green-500 text-white px-3 py-1 rounded-full"> | |
| {username} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex-grow overflow-hidden"> | |
| <canvas | |
| ref={canvasRef} | |
| onMouseDown={onMouseDown} | |
| onMouseMove={onMouseMove} | |
| onMouseUp={onMouseUp} | |
| onMouseLeave={onMouseUp} | |
| className="touch-none w-full h-full cursor-crosshair" | |
| /> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { Server } = require("socket.io"); | |
| const io = new Server(3001, { | |
| cors: { | |
| origin: "*", | |
| methods: ["GET", "POST"], | |
| }, | |
| }); | |
| const users = new Set(); | |
| io.on("connection", (socket) => { | |
| console.log("User connected:", socket.id); | |
| socket.on("user-join", (username) => { | |
| users.add(username); | |
| io.emit("update-users", Array.from(users)); | |
| socket.username = username; | |
| }); | |
| socket.on("draw-line", (data) => { | |
| socket.broadcast.emit("draw-line", data); | |
| }); | |
| socket.on("clear-canvas", () => { | |
| socket.broadcast.emit("clear-canvas"); | |
| }); | |
| socket.on("disconnect", () => { | |
| console.log("User disconnected:", socket.id); | |
| if (socket.username) { | |
| users.delete(socket.username); | |
| io.emit("update-users", Array.from(users)); | |
| } | |
| }); | |
| }); | |
| console.log("Socket.IO server running on port 3001"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment