import React, { useEffect, useRef, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import { fetchIslands } from '../../api/client'; import { useAuth } from '../../auth/AuthContext'; import './IslandsWorld.css'; /** * IslandsWorld: Full-screen metaverse for personal islands * Each user has their own floating island in this virtual ocean */ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPlanets }) { const { t } = useTranslation(); const { authenticated, username } = useAuth(); const containerRef = useRef(null); const mapRef = useRef(null); const markersRef = useRef([]); const animationRef = useRef(null); const [islands, setIslands] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [hoveredIsland, setHoveredIsland] = useState(null); // Fetch islands useEffect(() => { let mounted = true; setLoading(true); fetchIslands({ limit: 200 }) .then(data => { if (mounted) { setIslands(data || []); setLoading(false); } }) .catch(err => { console.warn('[IslandsWorld] Failed to fetch islands:', err); if (mounted) setLoading(false); }); return () => { mounted = false; }; }, []); // Filter islands by search const filteredIslands = searchQuery ? islands.filter(i => (i.username || '').toLowerCase().includes(searchQuery.toLowerCase()) || (i.display_name || '').toLowerCase().includes(searchQuery.toLowerCase()) ) : islands; // Initialize MapLibre useEffect(() => { if (!containerRef.current || loading) return; const centerLng = 0; const centerLat = 0; const map = new maplibregl.Map({ container: containerRef.current, style: { version: 8, sources: {}, layers: [ { id: 'background', type: 'background', paint: { 'background-color': '#0a1628' // Deep ocean } } ] }, center: [centerLng, centerLat], zoom: 1, pitch: 50, bearing: 0, antialias: true, attributionControl: false }); mapRef.current = map; map.on('load', () => { // Add ocean gradient layer const oceanGradientData = createOceanGradient(centerLng, centerLat); map.addSource('ocean-gradient', { type: 'geojson', data: oceanGradientData }); map.addLayer({ id: 'ocean-glow', type: 'fill', source: 'ocean-gradient', paint: { 'fill-color': [ 'interpolate', ['linear'], ['get', 'intensity'], 0, '#0a1628', 0.5, '#0d2847', 1, '#1e3a5f' ], 'fill-opacity': 0.8 } }); // Add starfield const starsData = createStarfield(500); map.addSource('stars', { type: 'geojson', data: starsData }); map.addLayer({ id: 'stars', type: 'circle', source: 'stars', paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 0, 1, 5, 3], 'circle-color': ['get', 'color'], 'circle-opacity': ['get', 'opacity'], 'circle-blur': 0.3 } }); // Create island markers const markers = []; const gridSize = Math.ceil(Math.sqrt(filteredIslands.length || 1)); const spacing = 20; filteredIslands.forEach((island, idx) => { const gridX = idx % gridSize; const gridY = Math.floor(idx / gridSize); const baseX = (gridX - gridSize / 2) * spacing; const baseY = (gridY - gridSize / 2) * spacing; const jitterX = (Math.random() - 0.5) * spacing * 0.4; const jitterY = (Math.random() - 0.5) * spacing * 0.4; const lng = centerLng + baseX + jitterX; const lat = centerLat + baseY + jitterY; const el = createIslandMarker(island, idx, t, username); el.addEventListener('mouseenter', () => setHoveredIsland(island)); el.addEventListener('mouseleave', () => setHoveredIsland(null)); el.addEventListener('click', (e) => { e.stopPropagation(); const action = e.target.closest('[data-action]')?.dataset?.action; if (action === 'profile') { // Navigate to user profile window.location.href = `/profile/${island.username}`; return; } if (action === 'edit') { // Navigate to island edit window.location.href = `/island/${island.username}/edit`; return; } // Default: open island viewer onIslandClick?.(island); }); const marker = new maplibregl.Marker({ element: el, anchor: 'center' }) .setLngLat([lng, lat]) .addTo(map); markers.push({ marker, island, el }); }); markersRef.current = markers; // Gentle rotation let bearing = 0; function animate() { bearing += 0.05; if (bearing >= 360) bearing = 0; map.rotateTo(bearing, { duration: 100 }); animationRef.current = requestAnimationFrame(animate); } animate(); }); return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current); markersRef.current.forEach(({ marker }) => marker.remove()); markersRef.current = []; map.remove(); }; }, [filteredIslands, loading, onIslandClick, t]); return (
{t('worlds.loadingIslands')}