Modernize collaborative pixel art platform to production-ready architecture
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
ee5b0bee92
commit
1da96f34a6
69 changed files with 17771 additions and 1589 deletions
146
frontend/src/components/ui/Toolbar.tsx
Normal file
146
frontend/src/components/ui/Toolbar.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
'use client';
|
||||
|
||||
import { useCanvasStore } from '../../store/canvasStore';
|
||||
import { useTheme } from '../ThemeProvider';
|
||||
|
||||
export function Toolbar() {
|
||||
const {
|
||||
selectedTool,
|
||||
setSelectedTool,
|
||||
brushSize,
|
||||
setBrushSize,
|
||||
showGrid,
|
||||
showCursors,
|
||||
viewport,
|
||||
setZoom,
|
||||
setViewport,
|
||||
} = useCanvasStore();
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const tools = [
|
||||
{ id: 'pixel', name: 'Pixel', icon: '🖊️' },
|
||||
{ id: 'fill', name: 'Fill', icon: '🪣' },
|
||||
{ id: 'eyedropper', name: 'Eyedropper', icon: '💉' },
|
||||
] as const;
|
||||
|
||||
const handleZoomIn = () => {
|
||||
setZoom(viewport.zoom * 1.2);
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
setZoom(viewport.zoom * 0.8);
|
||||
};
|
||||
|
||||
const handleResetView = () => {
|
||||
setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed top-6 left-1/2 transform -translate-x-1/2 z-40 flex flex-wrap items-center gap-2 md:gap-3 bg-gradient-to-r from-gray-900/95 to-black/90 backdrop-blur-xl rounded-2xl p-2 md:p-4 border border-white/10 shadow-2xl max-w-[calc(100vw-2rem)]">
|
||||
{/* Tools */}
|
||||
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 border border-white/10">
|
||||
{tools.map((tool) => (
|
||||
<button
|
||||
key={tool.id}
|
||||
className={`px-2 md:px-3 py-1 md:py-2 rounded-lg text-xs md:text-sm font-medium transition-all duration-200 ${
|
||||
selectedTool === tool.id
|
||||
? 'bg-blue-500 text-white shadow-lg'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
onClick={() => setSelectedTool(tool.id)}
|
||||
title={tool.name}
|
||||
>
|
||||
<span className="text-base md:mr-1">{tool.icon}</span>
|
||||
<span className="hidden md:inline">{tool.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Brush size (only for pixel tool) */}
|
||||
{selectedTool === 'pixel' && (
|
||||
<div className="flex items-center gap-3 bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<span className="text-sm text-white/80 font-medium">Size:</span>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={10}
|
||||
value={brushSize}
|
||||
onChange={(e) => setBrushSize(parseInt(e.target.value))}
|
||||
className="w-20 h-2 bg-white/20 rounded-lg appearance-none cursor-pointer"
|
||||
style={{
|
||||
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${(brushSize - 1) * 11.11}%, rgba(255,255,255,0.2) ${(brushSize - 1) * 11.11}%, rgba(255,255,255,0.2) 100%)`
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm text-white/80 font-medium min-w-[1.5rem] text-center">
|
||||
{brushSize}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zoom controls */}
|
||||
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 border border-white/10">
|
||||
<button
|
||||
className="px-3 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
|
||||
onClick={handleZoomOut}
|
||||
title="Zoom Out"
|
||||
>
|
||||
🔍➖
|
||||
</button>
|
||||
<div className="px-3 py-2 text-sm text-white/80 font-mono min-w-[4rem] text-center">
|
||||
{Math.round(viewport.zoom * 100)}%
|
||||
</div>
|
||||
<button
|
||||
className="px-3 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
|
||||
onClick={handleZoomIn}
|
||||
title="Zoom In"
|
||||
>
|
||||
🔍➕
|
||||
</button>
|
||||
<div className="w-px h-6 bg-white/20 mx-1" />
|
||||
<button
|
||||
className="px-3 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
|
||||
onClick={handleResetView}
|
||||
title="Reset View"
|
||||
>
|
||||
🏠
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* View options */}
|
||||
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 border border-white/10">
|
||||
<button
|
||||
className={`px-3 py-2 rounded-lg transition-all duration-200 ${
|
||||
showGrid
|
||||
? 'bg-green-500 text-white shadow-lg'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
onClick={() => useCanvasStore.setState({ showGrid: !showGrid })}
|
||||
title="Toggle Grid"
|
||||
>
|
||||
⬜
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-2 rounded-lg transition-all duration-200 ${
|
||||
showCursors
|
||||
? 'bg-purple-500 text-white shadow-lg'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
onClick={() => useCanvasStore.setState({ showCursors: !showCursors })}
|
||||
title="Toggle Cursors"
|
||||
>
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
className="px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
title="Toggle Theme"
|
||||
>
|
||||
{theme === 'dark' ? '☀️' : '🌙'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue