Rewrite with modern stack
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>
This commit is contained in:
parent
5eb7a1482e
commit
98f290a662
69 changed files with 17771 additions and 1589 deletions
113
frontend/src/components/canvas/CanvasContainer.tsx
Normal file
113
frontend/src/components/canvas/CanvasContainer.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
'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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue