'use client'; import { useState, useCallback, useEffect } from 'react'; import { VirtualCanvas } from '../components/canvas/VirtualCanvas'; import { CooldownTimer } from '../components/ui/CooldownTimer'; import { PixelConfirmModal } from '../components/ui/PixelConfirmModal'; import { StatsOverlay } from '../components/ui/StatsOverlay'; import { CoordinateDisplay } from '../components/ui/CoordinateDisplay'; import { UsernameModal } from '../components/ui/UsernameModal'; import { SettingsButton } from '../components/ui/SettingsButton'; import { ZoomControls } from '../components/ui/ZoomControls'; import { useWebSocket } from '../hooks/useWebSocket'; import { useCanvasStore } from '../store/canvasStore'; import { ErrorBoundary } from '../components/ErrorBoundary'; import type { PixelPlacedMessage, ChunkDataMessage } from '@gaplace/shared'; export default function HomePage() { const [selectedColor, setSelectedColor] = useState('#FF0000'); const [pendingPixel, setPendingPixel] = useState<{ x: number; y: number } | null>(null); const [isCooldownActive, setIsCooldownActive] = useState(false); const [onlineUsers, setOnlineUsers] = useState(1); const [totalPixels, setTotalPixels] = useState(0); const [hoverCoords, setHoverCoords] = useState<{ x: number; y: number; pixelInfo: { color: string; userId?: string; username?: string } | null } | null>(null); const [username, setUsername] = useState(''); const [showUsernameModal, setShowUsernameModal] = useState(false); // Generate userId once and keep it stable, store in localStorage const [userId] = useState(() => { if (typeof window !== 'undefined') { let storedUserId = localStorage.getItem('gaplace-user-id'); if (!storedUserId) { storedUserId = 'guest-' + Math.random().toString(36).substr(2, 9); localStorage.setItem('gaplace-user-id', storedUserId); } return storedUserId; } return 'guest-' + Math.random().toString(36).substr(2, 9); }); // Load username from localStorage and show modal if none exists useEffect(() => { if (typeof window !== 'undefined') { const storedUsername = localStorage.getItem('gaplace-username'); if (storedUsername) { setUsername(storedUsername); } else { setShowUsernameModal(true); } } }, []); // Canvas store const { setPixel, loadChunk: loadChunkToStore, viewport, setZoom, setViewport } = useCanvasStore(); const handlePixelPlaced = useCallback((message: PixelPlacedMessage & { username?: string }) => { console.log('Pixel placed:', message); setPixel(message.x, message.y, message.color, message.userId, message.username); }, [setPixel]); const handleChunkData = useCallback((message: ChunkDataMessage) => { console.log('Chunk data received:', message); loadChunkToStore(message.chunkX, message.chunkY, message.pixels); }, [loadChunkToStore]); const handleUserList = useCallback((users: string[]) => { setOnlineUsers(users.length); }, []); const handleCanvasStats = useCallback((stats: { totalPixels?: number; activeUsers?: number; lastActivity?: number }) => { setTotalPixels(stats.totalPixels || 0); }, []); const { isConnected, placePixel, loadChunk, moveCursor } = useWebSocket({ canvasId: 'main', userId, username, onPixelPlaced: handlePixelPlaced, onChunkData: handleChunkData, onUserList: handleUserList, onCanvasStats: handleCanvasStats, }); const handlePixelClick = useCallback((x: number, y: number) => { if (isCooldownActive) return; setPendingPixel({ x, y }); }, [isCooldownActive]); const handleConfirmPixel = useCallback(() => { if (!pendingPixel) return; // Immediately place pixel locally for instant feedback setPixel(pendingPixel.x, pendingPixel.y, selectedColor, userId, username); // Send to server placePixel(pendingPixel.x, pendingPixel.y, selectedColor); setPendingPixel(null); setIsCooldownActive(true); }, [pendingPixel, selectedColor, placePixel, setPixel, userId, username]); const handleCancelPixel = useCallback(() => { setPendingPixel(null); }, []); const handleCooldownComplete = useCallback(() => { setIsCooldownActive(false); }, []); const handleCursorMove = useCallback((x: number, y: number) => { moveCursor(x, y, 'pixel'); }, [moveCursor]); const handleChunkNeeded = useCallback((chunkX: number, chunkY: number) => { loadChunk(chunkX, chunkY); }, [loadChunk]); const handleHoverChange = useCallback((x: number, y: number, pixelInfo: { color: string; userId?: string } | null) => { setHoverCoords({ x, y, pixelInfo }); }, []); const handleUsernameChange = useCallback((newUsername: string) => { setUsername(newUsername); if (typeof window !== 'undefined') { localStorage.setItem('gaplace-username', newUsername); } }, []); const handleZoomIn = useCallback(() => { const newZoom = Math.min(viewport.zoom * 1.2, 5.0); // Zoom towards center of viewport (canvas center) if (typeof window !== 'undefined') { const screenCenterX = window.innerWidth / 2; const screenCenterY = window.innerHeight / 2; // Calculate what canvas coordinate is currently at screen center const BASE_PIXEL_SIZE = 32; const currentPixelSize = BASE_PIXEL_SIZE * viewport.zoom; const newPixelSize = BASE_PIXEL_SIZE * newZoom; const canvasCenterX = (screenCenterX + viewport.x) / currentPixelSize; const canvasCenterY = (screenCenterY + viewport.y) / currentPixelSize; // Calculate new viewport to keep same canvas point at screen center const newViewportX = canvasCenterX * newPixelSize - screenCenterX; const newViewportY = canvasCenterY * newPixelSize - screenCenterY; setViewport({ zoom: newZoom, x: newViewportX, y: newViewportY, }); } else { setZoom(newZoom); } }, [setZoom, setViewport, viewport]); const handleZoomOut = useCallback(() => { const newZoom = Math.max(viewport.zoom / 1.2, 0.1); // Zoom towards center of viewport (canvas center) if (typeof window !== 'undefined') { const screenCenterX = window.innerWidth / 2; const screenCenterY = window.innerHeight / 2; // Calculate what canvas coordinate is currently at screen center const BASE_PIXEL_SIZE = 32; const currentPixelSize = BASE_PIXEL_SIZE * viewport.zoom; const newPixelSize = BASE_PIXEL_SIZE * newZoom; const canvasCenterX = (screenCenterX + viewport.x) / currentPixelSize; const canvasCenterY = (screenCenterY + viewport.y) / currentPixelSize; // Calculate new viewport to keep same canvas point at screen center const newViewportX = canvasCenterX * newPixelSize - screenCenterX; const newViewportY = canvasCenterY * newPixelSize - screenCenterY; setViewport({ zoom: newZoom, x: newViewportX, y: newViewportY, }); } else { setZoom(newZoom); } }, [setZoom, setViewport, viewport]); return ( {/* Fullscreen Canvas */} 🎨 Canvas failed to load window.location.reload()} className="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors" > Reload }> {/* Overlay UI Components */} {/* Zoom Controls */} {/* Username Settings Button - Only show when no stats are visible */} {username && ( setShowUsernameModal(true)} /> )} {/* Coordinate Display */} {hoverCoords && ( )} {/* Username Modal */} setShowUsernameModal(false)} /> {/* Connection Status */} {!isConnected && ( Connecting... )} ); }