@@ -4,11 +4,10 @@ import { ColorPicker } from "./ColorPicker";
44import { StrokeControl } from "./StrokeControl" ;
55import { toast } from "sonner" ;
66import { io } from "socket.io-client" ;
7- import { jsPDF } from "jspdf" ;
87
98export const Canvas = ( ) => {
109 const canvasRef = useRef ( null ) ;
11- const [ activeTool , setActiveTool ] = useState ( "pen" ) ; // default = pen
10+ const [ activeTool , setActiveTool ] = useState ( "pen" ) ;
1211 const [ activeColor , setActiveColor ] = useState ( "#000000" ) ;
1312 const [ strokeWidth , setStrokeWidth ] = useState ( 3 ) ;
1413 const [ isDrawing , setIsDrawing ] = useState ( false ) ;
@@ -20,18 +19,19 @@ export const Canvas = () => {
2019
2120 // Collaboration
2221 const [ roomId , setRoomId ] = useState ( "" ) ;
23- const [ joined , setJoined ] = useState ( false ) ;
22+ const [ joined , setJoined ] = useState ( false ) ; // false = local mode
2423 const [ socket , setSocket ] = useState ( null ) ;
24+ const [ isModalOpen , setIsModalOpen ] = useState ( false ) ; // controls modal visibility
2525
26- // Initialize socket connection
26+ // Initialize socket connection (always available but idle until joined)
2727 useEffect ( ( ) => {
28- const s = io ( "http://localhost:3000" ) ; // replace with your backend URL
28+ const s = io ( "http://localhost:3000" ) ;
2929 setSocket ( s ) ;
3030
3131 s . on ( "connect" , ( ) => console . log ( "Connected to server:" , s . id ) ) ;
3232
33- // Handle strokes from other users
3433 s . on ( "draw" , ( { x, y, color, width, type, tool } ) => {
34+ if ( ! joined ) return ; // only listen when in collab mode
3535 const ctx = canvasRef . current ?. getContext ( "2d" ) ;
3636 if ( ! ctx ) return ;
3737
@@ -43,7 +43,7 @@ export const Canvas = () => {
4343 } ) ;
4444
4545 return ( ) => s . disconnect ( ) ;
46- } , [ ] ) ;
46+ } , [ joined ] ) ;
4747
4848 // Canvas setup
4949 useEffect ( ( ) => {
@@ -63,34 +63,24 @@ export const Canvas = () => {
6363 } ;
6464
6565 window . addEventListener ( "resize" , handleResize ) ;
66- toast . success ( "Canvas ready! Start drawing! " ) ;
66+ toast . success ( "Canvas ready! Local mode active. " ) ;
6767 return ( ) => window . removeEventListener ( "resize" , handleResize ) ;
6868 } , [ ] ) ;
6969
7070 // Keyboard shortcuts
7171 useEffect ( ( ) => {
7272 const handleKeyDown = ( e ) => {
7373 if ( ! isCanvasFocused ) return ;
74-
75- if ( e . key === "p" || e . key === "P" || e . key === "1" ) {
76- handleToolChange ( "pen" ) ;
77- } else if ( e . key === "e" || e . key === "E" || e . key === "2" ) {
78- handleToolChange ( "eraser" ) ;
79- } else if ( e . key === "l" || e . key === "L" || e . key === "3" ) {
80- handleToolChange ( "line" ) ;
81- }
74+ if ( e . key === "p" || e . key === "P" || e . key === "1" ) handleToolChange ( "pen" ) ;
75+ else if ( e . key === "e" || e . key === "E" || e . key === "2" ) handleToolChange ( "eraser" ) ;
76+ else if ( e . key === "l" || e . key === "L" || e . key === "3" ) handleToolChange ( "line" ) ;
8277 } ;
83-
8478 window . addEventListener ( "keydown" , handleKeyDown ) ;
8579 return ( ) => window . removeEventListener ( "keydown" , handleKeyDown ) ;
8680 } , [ isCanvasFocused ] ) ;
8781
8882 // Drawing logic
8983 const startDrawing = ( e ) => {
90- if ( ! joined ) {
91- toast . error ( "Join a room first!" ) ;
92- return ;
93- }
9484 const canvas = canvasRef . current ;
9585 const ctx = canvas ?. getContext ( "2d" ) ;
9686 if ( ! ctx ) return ;
@@ -105,15 +95,17 @@ export const Canvas = () => {
10595 if ( activeTool === "pen" || activeTool === "eraser" ) {
10696 ctx . beginPath ( ) ;
10797 ctx . moveTo ( x , y ) ;
108- socket . emit ( "draw" , {
109- roomId,
110- x,
111- y,
112- type : "start" ,
113- color : activeColor ,
114- width : strokeWidth ,
115- tool : activeTool ,
116- } ) ;
98+
99+ if ( joined && socket )
100+ socket . emit ( "draw" , {
101+ roomId,
102+ x,
103+ y,
104+ type : "start" ,
105+ color : activeColor ,
106+ width : strokeWidth ,
107+ tool : activeTool ,
108+ } ) ;
117109 }
118110
119111 if ( activeTool === "line" ) {
@@ -137,15 +129,16 @@ export const Canvas = () => {
137129 ctx . lineTo ( x , y ) ;
138130 ctx . stroke ( ) ;
139131
140- socket . emit ( "draw" , {
141- roomId,
142- x,
143- y,
144- type : "move" ,
145- color : activeColor ,
146- width : strokeWidth ,
147- tool : activeTool ,
148- } ) ;
132+ if ( joined && socket )
133+ socket . emit ( "draw" , {
134+ roomId,
135+ x,
136+ y,
137+ type : "move" ,
138+ color : activeColor ,
139+ width : strokeWidth ,
140+ tool : activeTool ,
141+ } ) ;
149142 } else if ( activeTool === "line" ) {
150143 ctx . putImageData ( snapshot . current , 0 , 0 ) ;
151144 ctx . beginPath ( ) ;
@@ -175,62 +168,20 @@ export const Canvas = () => {
175168 if ( ! roomId . trim ( ) || ! socket ) return ;
176169 socket . emit ( "join-room" , roomId . trim ( ) ) ;
177170 setJoined ( true ) ;
178- toast . success ( `Joined room: ${ roomId } ` ) ;
171+ setIsModalOpen ( false ) ;
172+ toast . success ( `Collaborative mode active - joined room: ${ roomId } ` ) ;
179173 } ;
180174
181175 const handleToolChange = ( tool ) => {
182176 setActiveTool ( tool ) ;
183177 toast . info ( `${ tool . charAt ( 0 ) . toUpperCase ( ) + tool . slice ( 1 ) } tool selected` ) ;
184178 } ;
185179
186- const handleExport = ( format ) => {
187- const canvas = canvasRef . current ;
188- if ( ! canvas ) return ;
189-
190- try {
191- if ( format === "png" ) {
192- canvas . toBlob ( ( blob ) => {
193- const url = URL . createObjectURL ( blob ) ;
194- const link = document . createElement ( "a" ) ;
195- link . href = url ;
196- link . download = `canvas-${ Date . now ( ) } .png` ;
197- link . click ( ) ;
198- URL . revokeObjectURL ( url ) ;
199- toast . success ( "Canvas exported as PNG!" ) ;
200- } ) ;
201- } else if ( format === "svg" ) {
202- // Create SVG from canvas
203- const svgString = `
204- <svg xmlns="http://www.w3.org/2000/svg" width="${ canvas . width } " height="${ canvas . height } ">
205- <foreignObject width="100%" height="100%">
206- <img xmlns="http://www.w3.org/1999/xhtml" src="${ canvas . toDataURL ( ) } " width="${ canvas . width } " height="${ canvas . height } "/>
207- </foreignObject>
208- </svg>
209- ` ;
210- const blob = new Blob ( [ svgString ] , { type : "image/svg+xml" } ) ;
211- const url = URL . createObjectURL ( blob ) ;
212- const link = document . createElement ( "a" ) ;
213- link . href = url ;
214- link . download = `canvas-${ Date . now ( ) } .svg` ;
215- link . click ( ) ;
216- URL . revokeObjectURL ( url ) ;
217- toast . success ( "Canvas exported as SVG!" ) ;
218- } else if ( format === "pdf" ) {
219- // Export as PDF using jsPDF
220- const imgData = canvas . toDataURL ( "image/png" ) ;
221- const pdf = new jsPDF ( {
222- orientation : canvas . width > canvas . height ? "landscape" : "portrait" ,
223- unit : "px" ,
224- format : [ canvas . width , canvas . height ] ,
225- } ) ;
226-
227- pdf . addImage ( imgData , "PNG" , 0 , 0 , canvas . width , canvas . height ) ;
228- pdf . save ( `canvas-${ Date . now ( ) } .pdf` ) ;
229- toast . success ( "Canvas exported as PDF!" ) ;
230- }
231- } catch ( error ) {
232- toast . error ( "Failed to export canvas!" ) ;
233- console . error ( "Export error:" , error ) ;
180+ const handleExitRoom = ( ) => {
181+ if ( socket ) {
182+ socket . emit ( "leave-room" , roomId ) ;
183+ setJoined ( false ) ;
184+ toast . success ( `Left room: ${ roomId } ` ) ;
234185 }
235186 } ;
236187
@@ -240,8 +191,22 @@ export const Canvas = () => {
240191 activeTool = { activeTool }
241192 onToolChange = { handleToolChange }
242193 onClear = { handleClear }
243- onExport = { handleExport }
244194 />
195+
196+ { joined ? < button
197+ onClick = { ( ) => handleExitRoom ( ) }
198+ className = "fixed top-4 right-4 bg-red-700 text-white px-4 py-2 rounded-lg shadow hover:bg-red-800 z-50"
199+ >
200+ Exit Room
201+ </ button >
202+ : < button
203+ onClick = { ( ) => setIsModalOpen ( true ) }
204+ className = "fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow hover:bg-blue-700 z-50"
205+ >
206+ Collaborate
207+ </ button > }
208+
209+
245210 < ColorPicker activeColor = { activeColor } onColorChange = { setActiveColor } />
246211 < StrokeControl
247212 strokeWidth = { strokeWidth }
@@ -260,7 +225,7 @@ export const Canvas = () => {
260225 className = "cursor-crosshair focus:outline-2 focus:outline-primary"
261226 />
262227
263- { ! joined && (
228+ { isModalOpen && (
264229 < div className = "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white/90 border border-gray-400 rounded-xl shadow-xl p-6 z-50 flex flex-col items-center gap-3" >
265230 < h2 className = "text-xl font-semibold" > Join a Room</ h2 >
266231 < input
@@ -276,13 +241,21 @@ export const Canvas = () => {
276241 >
277242 Join Room
278243 </ button >
244+ < button
245+ onClick = { ( ) => setIsModalOpen ( false ) }
246+ className = "text-sm text-gray-600 mt-2 hover:underline"
247+ >
248+ Cancel
249+ </ button >
279250 </ div >
280251 ) }
281252
282253 < div className = "fixed bottom-6 left-1/2 -translate-x-1/2 pointer-events-none" >
283254 < div className = "bg-toolbar/95 border border-toolbar-border rounded-xl shadow-lg backdrop-blur-sm px-6 py-3" >
284255 < p className = "text-sm text-foreground font-medium" >
285- Welcome to CollabCanvas — Select a tool and start drawing!
256+ { joined
257+ ? `Connected to Room: ${ roomId } `
258+ : "Local mode - your drawings are not shared." }
286259 </ p >
287260 </ div >
288261 </ div >
0 commit comments