diff --git a/package.json b/package.json index fc98810..e01cdc0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sociowire-frontend", "private": true, - "version": "0.0.110", + "version": "0.0.111", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/src/components/World/IslandsWorld.css b/src/components/World/IslandsWorld.css index 760e297..683a77a 100644 --- a/src/components/World/IslandsWorld.css +++ b/src/components/World/IslandsWorld.css @@ -19,10 +19,10 @@ /* Search bar - fixed below topbar */ .islands-world-search { position: fixed; - top: 56px; + top: 52px; left: 50%; transform: translateX(-50%); - z-index: 100; + z-index: 600; display: flex; align-items: center; gap: 6px; @@ -366,40 +366,26 @@ } } -/* Island Terrain View */ -.island-terrain-view { +/* Island Zoomed Overlay */ +.island-zoomed-overlay { position: fixed; - inset: 0; - z-index: 500; - background: linear-gradient(180deg, #0a1628 0%, #0d2847 100%); - animation: terrainFadeIn 0.4s ease; -} - -@keyframes terrainFadeIn { - from { - opacity: 0; - transform: scale(1.1); - } - to { - opacity: 1; - transform: scale(1); - } -} - -.island-terrain-header { - position: absolute; top: 0; left: 0; right: 0; - z-index: 100; + z-index: 500; + pointer-events: none; +} + +.island-zoomed-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; background: linear-gradient(180deg, rgba(10, 22, 40, 0.95) 0%, rgba(10, 22, 40, 0) 100%); + pointer-events: auto; } -.island-terrain-back { +.island-zoomed-back { display: flex; align-items: center; gap: 8px; @@ -414,35 +400,35 @@ transition: all 0.2s ease; } -.island-terrain-back:hover { +.island-zoomed-back:hover { background: rgba(56, 189, 248, 0.2); border-color: rgba(56, 189, 248, 0.5); } -.island-terrain-info { +.island-zoomed-info { text-align: center; } -.island-terrain-info h2 { +.island-zoomed-info h2 { margin: 0; font-size: 20px; font-weight: 700; color: #38bdf8; } -.island-terrain-type { +.island-zoomed-info span { font-size: 12px; color: rgba(255, 255, 255, 0.6); text-transform: uppercase; letter-spacing: 1px; } -.island-terrain-actions { +.island-zoomed-actions { display: flex; gap: 10px; } -.island-terrain-action { +.island-zoomed-action { display: flex; align-items: center; gap: 8px; @@ -456,90 +442,21 @@ transition: all 0.2s ease; } -.island-terrain-action:hover { +.island-zoomed-action:hover { background: rgba(255, 255, 255, 0.15); } -.island-terrain-post { +.island-zoomed-post { background: linear-gradient(135deg, #10b981, #059669); border: none; font-weight: 600; } -.island-terrain-post:hover { +.island-zoomed-post:hover { transform: scale(1.05); box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4); } -.island-terrain-canvas { - position: absolute; - inset: 0; -} - -.island-terrain-map { - width: 100%; - height: 100%; -} - -.island-terrain-loading { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 16px; -} - -.island-terrain-loading p { - color: rgba(255, 255, 255, 0.8); - font-size: 16px; -} - -/* Terrain post markers */ -.terrain-post-marker { - cursor: pointer; - transition: transform 0.2s ease; -} - -.terrain-post-marker:hover { - transform: scale(1.1); - z-index: 100; -} - -.terrain-post-icon { - width: 40px; - height: 40px; - background: linear-gradient(135deg, #38bdf8, #06b6d4); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 16px; - box-shadow: 0 4px 16px rgba(56, 189, 248, 0.5); -} - -.terrain-post-title { - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - margin-top: 4px; - padding: 4px 8px; - background: rgba(15, 23, 42, 0.9); - border-radius: 8px; - font-size: 11px; - color: white; - white-space: nowrap; - opacity: 0; - transition: opacity 0.2s; -} - -.terrain-post-marker:hover .terrain-post-title { - opacity: 1; -} - /* Responsive */ @media (max-width: 768px) { .islands-world-title { diff --git a/src/components/World/IslandsWorld.jsx b/src/components/World/IslandsWorld.jsx index 91e9475..1f0f089 100644 --- a/src/components/World/IslandsWorld.jsx +++ b/src/components/World/IslandsWorld.jsx @@ -14,21 +14,19 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl const { t } = useTranslation(); const { authenticated, username } = useAuth(); const containerRef = useRef(null); - const terrainRef = useRef(null); const mapRef = useRef(null); - const terrainMapRef = 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); - // Terrain view state + // Zoomed island state const [zoomedIsland, setZoomedIsland] = useState(null); - const [islandPosts, setIslandPosts] = useState([]); - const [terrainLoading, setTerrainLoading] = useState(false); + const [isZooming, setIsZooming] = useState(false); // Fetch islands useEffect(() => { @@ -58,34 +56,67 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl ) : islands; - // Load island posts when zooming into terrain view - useEffect(() => { - if (!zoomedIsland) { - setIslandPosts([]); - return; - } - setTerrainLoading(true); - fetchIslandPosts(zoomedIsland.username || zoomedIsland.id, { limit: 50 }) - .then(posts => { - setIslandPosts(posts || []); - setTerrainLoading(false); - }) - .catch(err => { - console.warn('[IslandsWorld] Failed to fetch island posts:', err); - setIslandPosts([]); - setTerrainLoading(false); - }); - }, [zoomedIsland]); - - // Handle zoom into island terrain + // Handle zoom to island const handleZoomToIsland = useCallback((island) => { - setZoomedIsland(island); - }, []); + if (!mapRef.current || isZooming) return; - // Handle back from terrain view - const handleBackFromTerrain = useCallback(() => { - setZoomedIsland(null); - }, []); + const pos = islandPositionsRef.current.get(island.username || island.id); + if (!pos) return; + + setIsZooming(true); + setZoomedIsland(island); + + // Stop the rotation animation + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + animationRef.current = null; + } + + // Fly to island position with high zoom + mapRef.current.flyTo({ + center: [pos.lng, pos.lat], + zoom: 6, + pitch: 60, + bearing: 0, + duration: 1500, + essential: true + }); + + mapRef.current.once('moveend', () => { + setIsZooming(false); + }); + }, [isZooming]); + + // Handle zoom out (back to world view) + const handleZoomOut = useCallback(() => { + if (!mapRef.current || isZooming) return; + + setIsZooming(true); + + mapRef.current.flyTo({ + center: [0, 0], + zoom: 1, + pitch: 50, + bearing: 0, + duration: 1500, + essential: true + }); + + mapRef.current.once('moveend', () => { + setZoomedIsland(null); + setIsZooming(false); + + // Restart rotation animation + let bearing = 0; + function animate() { + bearing += 0.05; + if (bearing >= 360) bearing = 0; + mapRef.current?.rotateTo(bearing, { duration: 100 }); + animationRef.current = requestAnimationFrame(animate); + } + animate(); + }); + }, [isZooming]); // Initialize MapLibre useEffect(() => { @@ -104,7 +135,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl id: 'background', type: 'background', paint: { - 'background-color': '#0a1628' // Deep ocean + 'background-color': '#0a1628' } } ] @@ -122,23 +153,14 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl map.on('load', () => { // Add ocean gradient layer const oceanGradientData = createOceanGradient(centerLng, centerLat); - map.addSource('ocean-gradient', { - type: 'geojson', - data: oceanGradientData - }); - + 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-color': ['interpolate', ['linear'], ['get', 'intensity'], + 0, '#0a1628', 0.5, '#0d2847', 1, '#1e3a5f' ], 'fill-opacity': 0.8 } @@ -174,6 +196,9 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl const lng = centerLng + baseX + jitterX; const lat = centerLat + baseY + 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)); @@ -182,16 +207,14 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl 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: zoom into island terrain + // Zoom to island handleZoomToIsland(island); }); @@ -207,7 +230,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl markersRef.current = markers; - // Gentle rotation + // Gentle rotation (only when not zoomed) let bearing = 0; function animate() { bearing += 0.05; @@ -222,9 +245,10 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl if (animationRef.current) cancelAnimationFrame(animationRef.current); markersRef.current.forEach(({ marker }) => marker.remove()); markersRef.current = []; + islandPositionsRef.current.clear(); map.remove(); }; - }, [filteredIslands, loading, handleZoomToIsland, t, username]); + }, [filteredIslands, loading, t, username, handleZoomToIsland]); return (
@@ -257,8 +281,8 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl {/* Main canvas */}
- {/* Hovered island tooltip */} - {hoveredIsland && ( + {/* Hovered island tooltip (only when not zoomed) */} + {hoveredIsland && !zoomedIsland && (
{hoveredIsland.avatar_url ? ( @@ -277,33 +301,24 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
)} - - {/* Loading overlay */} - {loading && ( -
-
-

{t('worlds.loadingIslands')}

-
- )} - - {/* Island Terrain View - Full screen when zoomed */} - {zoomedIsland && ( -
-
- -
-

@{zoomedIsland.username}'s Island

- {zoomedIsland.terrain_type || 'tropical'} +
+

@{zoomedIsland.username}

+ {zoomedIsland.terrain_type || 'tropical'}
-
- {username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && ( -
+
+ )} - {/* Terrain canvas with posts */} -
- {terrainLoading ? ( -
-
-

{t('worlds.loadingTerrain')}

-
- ) : ( - - )} -
+ {/* Loading overlay */} + {loading && ( +
+
+

{t('worlds.loadingIslands')}

)}
); } -/** - * Island Terrain Canvas - renders the island as a large terrain with posts - */ -function IslandTerrainCanvas({ island, posts, isOwner }) { - const canvasRef = useRef(null); - const mapRef = useRef(null); - - const terrainColors = { - 'tropical': { main: '#2ecc71', sand: '#F5DEB3', water: '#1e90ff' }, - 'desert': { main: '#f39c12', sand: '#EDC9AF', water: '#4aa3df' }, - 'arctic': { main: '#3498db', sand: '#E8E8E8', water: '#5fa8d3' }, - 'volcanic': { main: '#e74c3c', sand: '#2C2C2C', water: '#ff6b6b' }, - 'meadow': { main: '#27ae60', sand: '#C2B280', water: '#5dade2' }, - }; - - const colors = terrainColors[island.terrain_type] || terrainColors['tropical']; - - useEffect(() => { - if (!canvasRef.current) return; - - const map = new maplibregl.Map({ - container: canvasRef.current, - style: { - version: 8, - sources: {}, - layers: [ - { - id: 'background', - type: 'background', - paint: { 'background-color': colors.water } - } - ] - }, - center: [0, 0], - zoom: 3, - pitch: 45, - bearing: 0, - attributionControl: false - }); - - mapRef.current = map; - - map.on('load', () => { - // Create island terrain shape - const seed = island.seed || (island.username || '').charCodeAt(0) || 42; - const terrainData = generateTerrainGeoJSON(seed, colors.main, colors.sand); - - map.addSource('terrain', { type: 'geojson', data: terrainData.island }); - map.addSource('beach', { type: 'geojson', data: terrainData.beach }); - - // Beach layer - map.addLayer({ - id: 'beach', - type: 'fill', - source: 'beach', - paint: { - 'fill-color': colors.sand, - 'fill-opacity': 0.9 - } - }); - - // Island terrain layer - map.addLayer({ - id: 'terrain', - type: 'fill', - source: 'terrain', - paint: { - 'fill-color': colors.main, - 'fill-opacity': 1 - } - }); - - // Add terrain details (trees, rocks, etc.) - const detailsData = generateTerrainDetails(seed); - map.addSource('details', { type: 'geojson', data: detailsData }); - map.addLayer({ - id: 'details', - type: 'circle', - source: 'details', - paint: { - 'circle-radius': ['get', 'size'], - 'circle-color': ['get', 'color'], - 'circle-opacity': 0.8 - } - }); - - // Add posts as markers on the terrain - posts.forEach((post, idx) => { - const angle = (idx / Math.max(posts.length, 1)) * Math.PI * 2; - const radius = 0.01 + Math.random() * 0.02; - const lng = Math.cos(angle) * radius; - const lat = Math.sin(angle) * radius; - - const el = document.createElement('div'); - el.className = 'terrain-post-marker'; - el.innerHTML = ` -
- -
-
${(post.title || post.caption || '').slice(0, 30)}...
- `; - el.addEventListener('click', () => { - window.dispatchEvent(new CustomEvent('sw:openPostDetail', { detail: { post } })); - }); - - new maplibregl.Marker({ element: el, anchor: 'bottom' }) - .setLngLat([lng, lat]) - .addTo(map); - }); - }); - - return () => { - map.remove(); - }; - }, [island, posts, colors]); - - return
; -} - -/** - * Generate terrain GeoJSON for island view - */ -function generateTerrainGeoJSON(seed, mainColor, sandColor) { - const rng = seededRandom(seed); - const numPoints = 24; - const islandPoints = []; - const beachPoints = []; - - for (let i = 0; i < numPoints; i++) { - const angle = (i / numPoints) * Math.PI * 2; - const islandRadius = 0.025 + rng() * 0.015; - const beachRadius = islandRadius * 1.15; - - islandPoints.push([Math.cos(angle) * islandRadius, Math.sin(angle) * islandRadius]); - beachPoints.push([Math.cos(angle) * beachRadius, Math.sin(angle) * beachRadius]); - } - - // Close the polygons - islandPoints.push(islandPoints[0]); - beachPoints.push(beachPoints[0]); - - return { - island: { - type: 'Feature', - geometry: { type: 'Polygon', coordinates: [islandPoints] } - }, - beach: { - type: 'Feature', - geometry: { type: 'Polygon', coordinates: [beachPoints] } - } - }; -} - -/** - * Generate terrain details (trees, rocks, flowers) - */ -function generateTerrainDetails(seed) { - const rng = seededRandom(seed); - const features = []; - const detailCount = 30 + Math.floor(rng() * 20); - - for (let i = 0; i < detailCount; i++) { - const angle = rng() * Math.PI * 2; - const radius = rng() * 0.02; - const lng = Math.cos(angle) * radius; - const lat = Math.sin(angle) * radius; - - const type = rng(); - let color, size; - if (type < 0.5) { - // Tree - color = '#228B22'; - size = 4 + rng() * 3; - } else if (type < 0.75) { - // Rock - color = '#696969'; - size = 2 + rng() * 2; - } else { - // Flower - color = ['#FF69B4', '#FFD700', '#FF6347', '#9370DB'][Math.floor(rng() * 4)]; - size = 2 + rng(); - } - - features.push({ - type: 'Feature', - geometry: { type: 'Point', coordinates: [lng, lat] }, - properties: { color, size } - }); - } - - return { type: 'FeatureCollection', features }; -} - /** * Create a stylized island marker element */ @@ -545,8 +359,6 @@ function createIslandMarker(island, idx, t, currentUsername) { }; const color = terrainColors[island.terrain_type] || terrainColors['tropical']; const seed = island.seed || idx; - - // Generate unique island shape const islandSVG = generateIslandSVG(seed, color); const postCount = island.content_count || 0; const isOwner = currentUsername && island.username && @@ -585,19 +397,14 @@ function generateIslandSVG(seed, color) { const cy = size / 2; const numPoints = 10; - // Generate organic shape 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 - }); + points.push({ x: cx + Math.cos(angle) * r, y: cy + Math.sin(angle) * r }); } - // Create smooth path let path = `M ${points[0].x} ${points[0].y}`; for (let i = 0; i < numPoints; i++) { const curr = points[i]; @@ -608,7 +415,6 @@ function generateIslandSVG(seed, color) { } path += ' Z'; - // Add decorations const numTrees = 3 + Math.floor(rng() * 4); let trees = ''; for (let i = 0; i < numTrees; i++) { @@ -635,25 +441,16 @@ function generateIslandSVG(seed, color) { - - - - + - - - ${trees} `; } -/** - * Create starfield GeoJSON - */ function createStarfield(count) { const features = []; for (let i = 0; i < count; i++) { @@ -672,9 +469,6 @@ function createStarfield(count) { return { type: 'FeatureCollection', features }; } -/** - * Create ocean gradient GeoJSON - */ function createOceanGradient(centerLng, centerLat) { const features = []; const rings = 10; @@ -683,10 +477,7 @@ function createOceanGradient(centerLng, centerLat) { 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 - ]); + coords.push([centerLng + Math.cos(angle) * radius, centerLat + Math.sin(angle) * radius]); } features.push({ type: 'Feature', @@ -697,9 +488,6 @@ function createOceanGradient(centerLng, centerLat) { return { type: 'FeatureCollection', features }; } -/** - * Seeded random number generator - */ function seededRandom(seed) { let s = seed; return function() { diff --git a/src/components/World/PlanetsWorld.css b/src/components/World/PlanetsWorld.css index 29ebec0..8aebfb9 100644 --- a/src/components/World/PlanetsWorld.css +++ b/src/components/World/PlanetsWorld.css @@ -19,10 +19,10 @@ /* Search bar - fixed below topbar */ .planets-world-search { position: fixed; - top: 56px; + top: 52px; left: 50%; transform: translateX(-50%); - z-index: 100; + z-index: 600; display: flex; align-items: center; gap: 6px; diff --git a/src/components/World/PlanetsWorld.jsx b/src/components/World/PlanetsWorld.jsx index d5a326f..e79d6de 100644 --- a/src/components/World/PlanetsWorld.jsx +++ b/src/components/World/PlanetsWorld.jsx @@ -2,13 +2,12 @@ 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 { fetchPlanets } from '../../api/client'; +import { fetchPlanets, createPlanet } from '../../api/client'; import { useAuth } from '../../auth/AuthContext'; import './PlanetsWorld.css'; /** * PlanetsWorld: Full-screen metaverse for group spaces (planets) - * Each planet represents a community/group floating in space */ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { const { t } = useTranslation(); @@ -17,16 +16,23 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { const mapRef = useRef(null); const markersRef = useRef([]); const animationRef = useRef(null); + const planetPositionsRef = useRef(new Map()); const [planets, setPlanets] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [hoveredPlanet, setHoveredPlanet] = useState(null); - // Terrain view state + // Zoomed planet state const [zoomedPlanet, setZoomedPlanet] = useState(null); - const [planetPosts, setPlanetPosts] = useState([]); - const [terrainLoading, setTerrainLoading] = useState(false); + const [isZooming, setIsZooming] = useState(false); + + // Create planet modal + const [showCreateModal, setShowCreateModal] = useState(false); + const [newPlanetName, setNewPlanetName] = useState(''); + const [newPlanetDesc, setNewPlanetDesc] = useState(''); + const [newPlanetTheme, setNewPlanetTheme] = useState('default'); + const [creating, setCreating] = useState(false); // Fetch planets useEffect(() => { @@ -56,54 +62,101 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { ) : planets; - // Load planet posts when zooming into terrain view - useEffect(() => { - if (!zoomedPlanet) { - setPlanetPosts([]); - return; + // Handle create planet + const handleCreatePlanet = async () => { + if (!newPlanetName.trim() || creating) return; + setCreating(true); + + const result = await createPlanet({ + name: newPlanetName.trim(), + description: newPlanetDesc.trim(), + theme: newPlanetTheme + }); + + if (result.ok || result.id) { + // Add new planet to list + setPlanets(prev => [result, ...prev]); + setShowCreateModal(false); + setNewPlanetName(''); + setNewPlanetDesc(''); + setNewPlanetTheme('default'); + } else { + alert(result.error || 'Failed to create planet'); } - setTerrainLoading(true); - // TODO: Add fetchPlanetPosts API function - // For now, simulate with empty posts - setTimeout(() => { - setPlanetPosts([]); - setTerrainLoading(false); - }, 500); - }, [zoomedPlanet]); + setCreating(false); + }; - // Handle zoom into planet terrain + // Handle zoom to planet const handleZoomToPlanet = useCallback((planet) => { - setZoomedPlanet(planet); - }, []); + if (!mapRef.current || isZooming) return; - // Handle back from terrain view - const handleBackFromTerrain = useCallback(() => { - setZoomedPlanet(null); - }, []); + const pos = planetPositionsRef.current.get(planet.id || planet.name); + if (!pos) return; + + setIsZooming(true); + setZoomedPlanet(planet); + + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + animationRef.current = null; + } + + mapRef.current.flyTo({ + center: [pos.lng, pos.lat], + zoom: 4, + pitch: 60, + bearing: 0, + duration: 1500, + essential: true + }); + + mapRef.current.once('moveend', () => { + setIsZooming(false); + }); + }, [isZooming]); + + // Handle zoom out + const handleZoomOut = useCallback(() => { + if (!mapRef.current || isZooming) return; + + setIsZooming(true); + + mapRef.current.flyTo({ + center: [0, 0], + zoom: 0.8, + pitch: 45, + bearing: 0, + duration: 1500, + essential: true + }); + + mapRef.current.once('moveend', () => { + setZoomedPlanet(null); + setIsZooming(false); + + let bearing = 0; + function animate() { + bearing += 0.03; + if (bearing >= 360) bearing = 0; + mapRef.current?.rotateTo(bearing, { duration: 100 }); + animationRef.current = requestAnimationFrame(animate); + } + animate(); + }); + }, [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': '#050510' // Deep space black - } - } - ] + layers: [{ id: 'background', type: 'background', paint: { 'background-color': '#050510' } }] }, - center: [centerLng, centerLat], + center: [0, 0], zoom: 0.8, pitch: 45, bearing: 0, @@ -114,26 +167,19 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { mapRef.current = map; map.on('load', () => { - // Add nebula effect + // Nebula effect const nebulaData = createNebulaEffect(); map.addSource('nebula', { type: 'geojson', data: nebulaData }); map.addLayer({ - id: 'nebula', - type: 'fill', - source: 'nebula', - paint: { - 'fill-color': ['get', 'color'], - 'fill-opacity': ['get', 'opacity'] - } + id: 'nebula', type: 'fill', source: 'nebula', + paint: { 'fill-color': ['get', 'color'], 'fill-opacity': ['get', 'opacity'] } }); - // Add starfield + // Starfield const starsData = createStarfield(800); map.addSource('stars', { type: 'geojson', data: starsData }); map.addLayer({ - id: 'stars', - type: 'circle', - source: 'stars', + id: 'stars', type: 'circle', source: 'stars', paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 0, 0.8, 5, 2.5], 'circle-color': ['get', 'color'], @@ -142,20 +188,19 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { } }); - // Create planet markers in orbital rings + // Planet markers const markers = []; - filteredPlanets.forEach((planet, idx) => { const ring = Math.floor(idx / 6); const posInRing = idx % 6; const baseRadius = 25 + ring * 30; const angle = (posInRing / 6) * Math.PI * 2 + (ring * 0.5); + const lng = Math.cos(angle) * baseRadius; + const lat = Math.sin(angle) * baseRadius; - const lng = centerLng + Math.cos(angle) * baseRadius; - const lat = centerLat + Math.sin(angle) * baseRadius; - - const el = createPlanetMarker(planet, idx, t); + planetPositionsRef.current.set(planet.id || planet.name, { lng, lat }); + const el = createPlanetMarker(planet, idx); el.addEventListener('mouseenter', () => setHoveredPlanet(planet)); el.addEventListener('mouseleave', () => setHoveredPlanet(null)); el.addEventListener('click', (e) => { @@ -163,10 +208,7 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { handleZoomToPlanet(planet); }); - const marker = new maplibregl.Marker({ - element: el, - anchor: 'center' - }) + const marker = new maplibregl.Marker({ element: el, anchor: 'center' }) .setLngLat([lng, lat]) .addTo(map); @@ -175,7 +217,7 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { markersRef.current = markers; - // Slow cosmic rotation + // Rotation let bearing = 0; function animate() { bearing += 0.03; @@ -190,13 +232,14 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) { if (animationRef.current) cancelAnimationFrame(animationRef.current); markersRef.current.forEach(({ marker }) => marker.remove()); markersRef.current = []; + planetPositionsRef.current.clear(); map.remove(); }; - }, [filteredPlanets, loading, t, handleZoomToPlanet]); + }, [filteredPlanets, loading, handleZoomToPlanet]); return (
- {/* Search bar with icon and world nav */} + {/* Search bar */}
@@ -222,17 +265,17 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
- {/* Main canvas */} + {/* Canvas */}
- {/* Hovered planet tooltip */} + {/* Tooltip */} {hoveredPlanet && !zoomedPlanet && (
-
{hoveredPlanet.name || 'Unknown Planet'}
+
{hoveredPlanet.name || 'Planet'}
{hoveredPlanet.description || t('worlds.clickToExplore')}
{hoveredPlanet.member_count || 0} {t('worlds.members')} • {hoveredPlanet.theme || 'default'} @@ -241,243 +284,98 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
)} - {/* Loading overlay */} - {loading && ( -
-
-

{t('worlds.loadingPlanets')}

-
- )} - - {/* Planet Terrain View - Full screen when zoomed */} - {zoomedPlanet && ( -
-
- -
-

{zoomedPlanet.name || 'Planet'}

- {zoomedPlanet.theme || 'default'} +
+

{zoomedPlanet.name}

+ {zoomedPlanet.theme || 'default'}
-
- {authenticated && ( - )}
+
+ )} - {/* Terrain canvas with posts */} -
- {terrainLoading ? ( -
-
-

{t('worlds.loadingTerrain')}

-
- ) : ( - setShowCreateModal(true)}> + + {t('worlds.createPlanet')} + + )} + + {/* Create Planet Modal */} + {showCreateModal && ( +
setShowCreateModal(false)}> +
e.stopPropagation()}> + +

{t('worlds.createPlanet')}

+
+ setNewPlanetName(e.target.value)} + maxLength={50} /> - )} +