Major refactor from simple HTML/JS app to modern full-stack TypeScript application: ## Architecture Changes - Migrated to monorepo structure with workspaces (backend, frontend, shared) - Backend: Node.js + Express + TypeScript + Socket.IO - Frontend: Next.js 15.5 + React 19 + TypeScript + Tailwind CSS - Shared: Common types and utilities across packages ## Key Features Implemented - Real-time WebSocket collaboration via Socket.IO - Virtual canvas with chunked loading for performance - Modern UI with dark mode and responsive design - Mock database system for easy development (Redis/PostgreSQL compatible) - Comprehensive error handling and rate limiting - User presence and cursor tracking - Infinite canvas support with zoom/pan controls ## Performance Optimizations - Canvas virtualization - only renders visible viewport - Chunked pixel data loading (64x64 pixel chunks) - Optimized WebSocket protocol - Memory-efficient state management with Zustand ## Development Experience - Full TypeScript support across all packages - Hot reload for both frontend and backend - Docker support for production deployment - Comprehensive linting and formatting - Automated development server startup ## Fixed Issues - Corrected start script paths - Updated environment configuration - Fixed ESLint configuration issues - Ensured all dependencies are properly installed - Verified build process works correctly 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
113 lines
No EOL
3.9 KiB
TypeScript
113 lines
No EOL
3.9 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { VirtualCanvas } from './VirtualCanvas';
|
||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||
import { useCanvasStore } from '../../store/canvasStore';
|
||
import { PixelPlacedMessage, ChunkDataMessage } from '@gaplace/shared';
|
||
|
||
export function CanvasContainer() {
|
||
const [userId] = useState(() => `user_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`);
|
||
const canvasId = 'default'; // TODO: Make this dynamic
|
||
|
||
const {
|
||
selectedColor,
|
||
selectedTool,
|
||
setPixel,
|
||
loadChunk,
|
||
setUserCursor,
|
||
removeUserCursor,
|
||
setActiveUsers,
|
||
setStats,
|
||
} = useCanvasStore();
|
||
|
||
const { isConnected, connectionError, placePixel, loadChunk: requestChunk, moveCursor } = useWebSocket({
|
||
canvasId,
|
||
userId,
|
||
onPixelPlaced: (message: PixelPlacedMessage) => {
|
||
setPixel(message.x, message.y, message.color, message.userId);
|
||
},
|
||
onChunkData: (message: ChunkDataMessage) => {
|
||
loadChunk(message.chunkX, message.chunkY, message.pixels);
|
||
},
|
||
onUserList: (users: string[]) => {
|
||
setActiveUsers(users);
|
||
},
|
||
onCanvasStats: (stats: { totalPixels?: number; activeUsers?: number; lastActivity?: number; userPixels?: number }) => {
|
||
setStats(stats.totalPixels || 0, stats.userPixels || 0);
|
||
},
|
||
onCursorUpdate: (data: { userId: string; username: string; x: number; y: number; tool: string }) => {
|
||
setUserCursor(data.userId, data.x, data.y, data.username, '#ff0000');
|
||
},
|
||
});
|
||
|
||
const handlePixelClick = (x: number, y: number) => {
|
||
if (!isConnected) return;
|
||
|
||
if (selectedTool === 'pixel') {
|
||
placePixel(x, y, selectedColor);
|
||
}
|
||
};
|
||
|
||
const handleCursorMove = (x: number, y: number) => {
|
||
if (!isConnected) return;
|
||
moveCursor(x, y, selectedTool);
|
||
};
|
||
|
||
const handleChunkNeeded = (chunkX: number, chunkY: number) => {
|
||
if (!isConnected) return;
|
||
requestChunk(chunkX, chunkY);
|
||
};
|
||
|
||
const handleHoverChange = (x: number, y: number, pixelInfo: { color: string; userId?: string } | null) => {
|
||
// Handle pixel hover for tooltips or UI updates
|
||
};
|
||
|
||
if (connectionError) {
|
||
return (
|
||
<div className="w-full h-full flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||
<div className="text-center">
|
||
<div className="text-red-500 mb-2">⚠️ Connection Error</div>
|
||
<div className="text-sm text-gray-600 dark:text-gray-400">{connectionError}</div>
|
||
<button
|
||
onClick={() => window.location.reload()}
|
||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||
>
|
||
Refresh Page
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!isConnected) {
|
||
return (
|
||
<div className="w-full h-full flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||
<div className="text-gray-600 dark:text-gray-400">Connecting to GaPlace...</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||
<VirtualCanvas
|
||
onPixelClick={handlePixelClick}
|
||
onCursorMove={handleCursorMove}
|
||
onChunkNeeded={handleChunkNeeded}
|
||
onHoverChange={handleHoverChange}
|
||
selectedColor={selectedColor}
|
||
/>
|
||
|
||
{/* Connection status indicator */}
|
||
<div className="absolute top-4 right-4 flex items-center space-x-2 bg-black/20 backdrop-blur-sm rounded-lg px-3 py-1">
|
||
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-400' : 'bg-red-400'}`} />
|
||
<span className="text-sm text-white">
|
||
{isConnected ? 'Connected' : 'Disconnected'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |