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, fetchIslandPosts, fetchPosts } 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 islandPositionsRef = useRef(new Map()); // Store island positions
const [islands, setIslands] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [hoveredIsland, setHoveredIsland] = useState(null);
// Zoomed island state
const [zoomedIsland, setZoomedIsland] = useState(null);
const [isZooming, setIsZooming] = useState(false);
const [islandPosts, setIslandPosts] = useState([]);
const [loadingPosts, setLoadingPosts] = useState(false);
// 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;
// Handle zoom to island - shows full-screen island terrain overlay
const handleZoomToIsland = useCallback(async (island) => {
if (isZooming) return;
setIsZooming(true);
setZoomedIsland(island);
setLoadingPosts(true);
// Fetch posts for this island
try {
const posts = await fetchPosts({ username: island.username, limit: 50 });
setIslandPosts(posts || []);
} catch (err) {
console.warn('[IslandsWorld] Failed to fetch island posts:', err);
setIslandPosts([]);
}
setLoadingPosts(false);
setIsZooming(false);
}, [isZooming]);
// Handle zoom out (back to world view)
const handleZoomOut = useCallback(() => {
if (isZooming) return;
setZoomedIsland(null);
setIslandPosts([]);
setIsZooming(false);
}, [isZooming]);
// 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'
}
}
]
},
center: [centerLng, centerLat],
zoom: 2,
minZoom: 1,
maxZoom: 18,
pitch: 0,
bearing: 0,
antialias: true,
attributionControl: false,
dragRotate: false // Disable rotation, keep simple 2D navigation
});
mapRef.current = map;
// Add navigation controls
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'bottom-right');
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 - spread in a grid with some randomization
const markers = [];
const numIslands = filteredIslands.length;
const gridSize = Math.ceil(Math.sqrt(numIslands));
const spacing = 25; // Degrees between islands
filteredIslands.forEach((island, idx) => {
// Grid position with jitter
const gridX = idx % gridSize;
const gridY = Math.floor(idx / gridSize);
const rng = seededRandom((island.username || '').charCodeAt(0) || idx);
const jitterX = (rng() - 0.5) * spacing * 0.6;
const jitterY = (rng() - 0.5) * spacing * 0.6;
const lng = (gridX - gridSize / 2) * spacing + jitterX;
const lat = (gridY - gridSize / 2) * spacing + jitterY;
// Store position for zoom
islandPositionsRef.current.set(island.username || island.id, { lng, lat });
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') {
window.location.href = `/profile/${island.username}`;
return;
}
if (action === 'edit') {
window.location.href = `/island/${island.username}/edit`;
return;
}
// Zoom to island
handleZoomToIsland(island);
});
const marker = new maplibregl.Marker({
element: el,
anchor: 'center'
})
.setLngLat([lng, lat])
.addTo(map);
markers.push({ marker, island, el });
});
markersRef.current = markers;
});
return () => {
if (animationRef.current) cancelAnimationFrame(animationRef.current);
markersRef.current.forEach(({ marker }) => marker.remove());
markersRef.current = [];
islandPositionsRef.current.clear();
map.remove();
};
}, [filteredIslands, loading, t, username, handleZoomToIsland]);
return (
{/* Search bar with icon and world nav */}
setSearchQuery(e.target.value)}
/>
{searchQuery && (
)}
{islands.length}
{/* Main canvas */}
{/* Hovered island tooltip (only when not zoomed) */}
{hoveredIsland && !zoomedIsland && (
{hoveredIsland.avatar_url ? (

) : (
{(hoveredIsland.username || 'U')[0].toUpperCase()}
)}
@{hoveredIsland.username}
{hoveredIsland.display_name || hoveredIsland.bio || t('worlds.clickToVisit')}
{hoveredIsland.content_count || 0} {t('worlds.posts')} • {hoveredIsland.terrain_type || 'tropical'}
)}
{/* Zoomed island - full screen terrain view */}
{zoomedIsland && (
{/* Full-screen island terrain background */}
{/* Header bar */}
@{zoomedIsland.username}
{zoomedIsland.display_name || zoomedIsland.terrain_type || 'Island'}
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
<>
>
)}
{/* Posts on the island terrain */}
{loadingPosts && (
)}
{!loadingPosts && islandPosts.length === 0 && (
{t('worlds.emptyIsland')}
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
)}
)}
{!loadingPosts && islandPosts.map((post, idx) => (
))}
)}
{/* Loading overlay */}
{loading && (
{t('worlds.loadingIslands')}
)}
);
}
/**
* Create a stylized island marker element
*/
function createIslandMarker(island, idx, t, currentUsername) {
const el = document.createElement('div');
el.className = 'island-marker';
const terrainColors = {
'tropical': '#2ecc71',
'desert': '#f39c12',
'arctic': '#3498db',
'volcanic': '#e74c3c',
'meadow': '#27ae60',
};
const color = terrainColors[island.terrain_type] || terrainColors['tropical'];
const seed = island.seed || idx;
const islandSVG = generateIslandSVG(seed, color);
const postCount = island.content_count || 0;
const isOwner = currentUsername && island.username &&
currentUsername.toLowerCase() === island.username.toLowerCase();
el.innerHTML = `
${islandSVG}
@${island.username}
${postCount} posts
${isOwner ? `
` : ''}
`;
return el;
}
/**
* Generate island SVG with organic shape
*/
function generateIslandSVG(seed, color) {
const rng = seededRandom(seed);
const size = 140;
const cx = size / 2;
const cy = size / 2;
const numPoints = 10;
const points = [];
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const radiusVar = 0.55 + rng() * 0.45;
const r = (size / 2.5) * radiusVar;
points.push({ x: cx + Math.cos(angle) * r, y: cy + Math.sin(angle) * r });
}
let path = `M ${points[0].x} ${points[0].y}`;
for (let i = 0; i < numPoints; i++) {
const curr = points[i];
const next = points[(i + 1) % numPoints];
const cpx = (curr.x + next.x) / 2 + (rng() - 0.5) * 10;
const cpy = (curr.y + next.y) / 2 + (rng() - 0.5) * 10;
path += ` Q ${cpx} ${cpy} ${next.x} ${next.y}`;
}
path += ' Z';
const numTrees = 3 + Math.floor(rng() * 4);
let trees = '';
for (let i = 0; i < numTrees; i++) {
const angle = rng() * Math.PI * 2;
const dist = 10 + rng() * 20;
const tx = cx + Math.cos(angle) * dist;
const ty = cy + Math.sin(angle) * dist;
const treeSize = 6 + rng() * 6;
trees += `
`;
}
return `
`;
}
function createStarfield(count) {
const features = [];
for (let i = 0; i < count; i++) {
const lng = (Math.random() - 0.5) * 400;
const lat = (Math.random() - 0.5) * 400;
const brightness = Math.random();
features.push({
type: 'Feature',
geometry: { type: 'Point', coordinates: [lng, lat] },
properties: {
color: brightness > 0.8 ? '#ffeedd' : brightness > 0.5 ? '#aaccff' : '#ffffff',
opacity: 0.3 + brightness * 0.5
}
});
}
return { type: 'FeatureCollection', features };
}
function createOceanGradient(centerLng, centerLat) {
const features = [];
const rings = 10;
for (let i = 0; i < rings; i++) {
const radius = (i + 1) * 30;
const coords = [];
for (let j = 0; j <= 64; j++) {
const angle = (j / 64) * Math.PI * 2;
coords.push([centerLng + Math.cos(angle) * radius, centerLat + Math.sin(angle) * radius]);
}
features.push({
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [coords] },
properties: { intensity: 1 - (i / rings) }
});
}
return { type: 'FeatureCollection', features };
}
function seededRandom(seed) {
let s = seed;
return function() {
s = (s * 9301 + 49297) % 233280;
return s / 233280;
};
}
/**
* IslandTerrainBackground - Full-screen island shape background
*/
function IslandTerrainBackground({ island }) {
const terrainColors = {
'tropical': { main: '#2ecc71', sand: '#F5DEB3', water: '#1e3a5f' },
'desert': { main: '#f39c12', sand: '#DEB887', water: '#1e3a5f' },
'arctic': { main: '#3498db', sand: '#E0FFFF', water: '#0d2847' },
'volcanic': { main: '#e74c3c', sand: '#2C2C2C', water: '#1a1a2e' },
'meadow': { main: '#27ae60', sand: '#90EE90', water: '#0d2847' },
};
const colors = terrainColors[island.terrain_type] || terrainColors['tropical'];
const seed = island.seed || (island.username || '').charCodeAt(0) || 0;
return (
);
}
/**
* Generate organic island shape path
*/
function IslandShape({ seed, cx, cy, scale, fill, opacity = 1, filter }) {
const rng = seededRandom(seed);
const baseRadius = 30;
const numPoints = 12;
const points = [];
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const radiusVar = 0.7 + rng() * 0.3;
const r = baseRadius * radiusVar * scale;
points.push({
x: cx + Math.cos(angle) * r,
y: cy + Math.sin(angle) * r
});
}
let path = `M ${points[0].x} ${points[0].y}`;
for (let i = 0; i < numPoints; i++) {
const curr = points[i];
const next = points[(i + 1) % numPoints];
const cpx = (curr.x + next.x) / 2 + (rng() - 0.5) * 6;
const cpy = (curr.y + next.y) / 2 + (rng() - 0.5) * 6;
path += ` Q ${cpx} ${cpy} ${next.x} ${next.y}`;
}
path += ' Z';
return ;
}
/**
* Island trees and details
*/
function IslandDetails({ seed, cx, cy }) {
const rng = seededRandom(seed + 100);
const numTrees = 8 + Math.floor(rng() * 6);
const trees = [];
for (let i = 0; i < numTrees; i++) {
const angle = rng() * Math.PI * 2;
const dist = 5 + rng() * 18;
const tx = cx + Math.cos(angle) * dist;
const ty = cy + Math.sin(angle) * dist;
const treeSize = 2 + rng() * 3;
trees.push(
);
}
return {trees};
}
/**
* IslandPost - A post displayed on the island terrain
*/
function IslandPost({ post, index, islandSeed }) {
// Position posts pseudo-randomly on the island
const rng = seededRandom(islandSeed * 100 + index + (post.id || 0));
const left = 10 + rng() * 75; // 10-85% from left
const top = 15 + rng() * 65; // 15-80% from top
const rotation = (rng() - 0.5) * 12; // -6 to +6 degrees
const hasImage = post.media_urls?.length > 0 || post.image_url;
const imageUrl = post.media_urls?.[0] || post.image_url;
return (
window.location.href = `/post/${post.id}`}
>
{hasImage ? (
) : (
{(post.content || post.text || '').slice(0, 80)}
)}
{post.like_count || 0}
);
}