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:
martin 2025-08-22 19:28:05 +02:00
commit 98f290a662
69 changed files with 17771 additions and 1589 deletions

View 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>
);
}