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
This commit is contained in:
martin 2025-08-22 19:28:05 +02:00
commit 3ce5a97422
69 changed files with 17771 additions and 1589 deletions

10
shared/.eslintrc.json Normal file
View file

@ -0,0 +1,10 @@
{
"extends": ["../.eslintrc.json"],
"env": {
"browser": true,
"node": true
},
"parserOptions": {
"project": "./tsconfig.json"
}
}

20
shared/package.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "@gaplace/shared",
"version": "1.0.0",
"description": "Shared types and utilities for GaPlace",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"type-check": "tsc --noEmit",
"lint": "eslint src --ext .ts,.tsx"
},
"devDependencies": {
"typescript": "^5.3.3"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

View file

@ -0,0 +1,28 @@
export const CANVAS_CONFIG = {
DEFAULT_CHUNK_SIZE: 64,
MAX_CANVAS_SIZE: 10000,
MIN_CANVAS_SIZE: 100,
DEFAULT_CANVAS_SIZE: 1000,
MAX_ZOOM: 32,
MIN_ZOOM: 0.1,
DEFAULT_ZOOM: 1,
PIXEL_SIZE: 1, // Base pixel size in canvas units
} as const;
export const COLORS = {
DEFAULT: '#FFFFFF',
PALETTE: [
'#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF',
'#FFFF00', '#FF00FF', '#00FFFF', '#FFA500', '#800080',
'#FFC0CB', '#A52A2A', '#808080', '#90EE90', '#FFB6C1',
'#87CEEB', '#DDA0DD', '#98FB98', '#F0E68C', '#FF6347',
'#40E0D0'
]
} as const;
export const RATE_LIMITS = {
PIXELS_PER_MINUTE: 60,
PIXELS_PER_HOUR: 1000,
CURSOR_UPDATES_PER_SECOND: 10,
MAX_CONCURRENT_CHUNKS: 100,
} as const;

13
shared/src/index.ts Normal file
View file

@ -0,0 +1,13 @@
// Types
export * from './types/canvas';
export * from './types/user';
export * from './types/websocket';
// Constants
export * from './constants/canvas';
// Utils
export * from './utils/canvas';
// Re-export specific functions for convenience
export { isValidColor } from './utils/canvas';

View file

@ -0,0 +1,41 @@
export interface Pixel {
x: number;
y: number;
color: string;
timestamp: number;
userId?: string;
}
export interface PixelChunk {
chunkX: number;
chunkY: number;
pixels: Map<string, string>; // key: "x,y", value: color
lastModified: number;
}
export interface Canvas {
id: string;
name: string;
width: number;
height: number;
chunkSize: number;
isPublic: boolean;
createdAt: number;
updatedAt: number;
createdBy: string;
}
export interface Viewport {
x: number;
y: number;
width: number;
height: number;
zoom: number;
}
export interface CanvasMetadata {
totalPixels: number;
activeUsers: number;
lastActivity: number;
version: number;
}

34
shared/src/types/user.ts Normal file
View file

@ -0,0 +1,34 @@
export interface User {
id: string;
username: string;
email?: string;
avatar?: string;
isGuest: boolean;
createdAt: number;
lastSeen: number;
}
export interface UserSession {
userId: string;
sessionId: string;
canvasId: string;
isActive: boolean;
lastActivity: number;
cursor?: {
x: number;
y: number;
tool: string;
};
}
export interface UserPresence {
userId: string;
username: string;
cursor: {
x: number;
y: number;
};
color: string;
tool: string;
isActive: boolean;
}

View file

@ -0,0 +1,83 @@
export enum MessageType {
// Canvas operations
PLACE_PIXEL = 'PLACE_PIXEL',
PIXEL_PLACED = 'PIXEL_PLACED',
LOAD_CHUNK = 'LOAD_CHUNK',
CHUNK_DATA = 'CHUNK_DATA',
// User presence
USER_JOINED = 'USER_JOINED',
USER_LEFT = 'USER_LEFT',
CURSOR_MOVE = 'CURSOR_MOVE',
USER_LIST = 'USER_LIST',
// Canvas management
CANVAS_INFO = 'CANVAS_INFO',
CANVAS_UPDATED = 'CANVAS_UPDATED',
// System
HEARTBEAT = 'HEARTBEAT',
ERROR = 'ERROR',
RATE_LIMITED = 'RATE_LIMITED'
}
export interface BaseMessage {
type: MessageType;
timestamp: number;
userId?: string;
}
export interface PlacePixelMessage extends BaseMessage {
type: MessageType.PLACE_PIXEL;
x: number;
y: number;
color: string;
canvasId: string;
}
export interface PixelPlacedMessage extends BaseMessage {
type: MessageType.PIXEL_PLACED;
x: number;
y: number;
color: string;
userId: string;
canvasId: string;
}
export interface LoadChunkMessage extends BaseMessage {
type: MessageType.LOAD_CHUNK;
chunkX: number;
chunkY: number;
canvasId: string;
}
export interface ChunkDataMessage extends BaseMessage {
type: MessageType.CHUNK_DATA;
chunkX: number;
chunkY: number;
pixels: Array<{ x: number; y: number; color: string }>;
canvasId: string;
}
export interface CursorMoveMessage extends BaseMessage {
type: MessageType.CURSOR_MOVE;
x: number;
y: number;
tool: string;
canvasId: string;
}
export interface ErrorMessage extends BaseMessage {
type: MessageType.ERROR;
message: string;
code?: string;
}
export type WebSocketMessage =
| PlacePixelMessage
| PixelPlacedMessage
| LoadChunkMessage
| ChunkDataMessage
| CursorMoveMessage
| ErrorMessage
| BaseMessage;

View file

@ -0,0 +1,44 @@
import { CANVAS_CONFIG } from '../constants/canvas';
export function getChunkCoordinates(x: number, y: number, chunkSize = CANVAS_CONFIG.DEFAULT_CHUNK_SIZE) {
return {
chunkX: Math.floor(x / chunkSize),
chunkY: Math.floor(y / chunkSize),
};
}
export function getPixelKey(x: number, y: number): string {
return `${x},${y}`;
}
export function parsePixelKey(key: string): { x: number; y: number } {
const [x, y] = key.split(',').map(Number);
return { x, y };
}
export function getChunkKey(chunkX: number, chunkY: number): string {
return `${chunkX},${chunkY}`;
}
export function parseChunkKey(key: string): { chunkX: number; chunkY: number } {
const [chunkX, chunkY] = key.split(',').map(Number);
return { chunkX, chunkY };
}
export function getChunkBounds(chunkX: number, chunkY: number, chunkSize = CANVAS_CONFIG.DEFAULT_CHUNK_SIZE) {
return {
minX: chunkX * chunkSize,
minY: chunkY * chunkSize,
maxX: (chunkX + 1) * chunkSize - 1,
maxY: (chunkY + 1) * chunkSize - 1,
};
}
export function isValidColor(color: string): boolean {
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
return hexRegex.test(color);
}
export function clampCoordinate(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}

19
shared/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}