Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created March 4, 2025 11:44
Show Gist options
  • Select an option

  • Save shricodev/1a8171fdd875fb821fddfe88a76ba484 to your computer and use it in GitHub Desktop.

Select an option

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.
"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>
);
}
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