diff --git a/package-lock.json b/package-lock.json
index 5a24b67..c13ae2f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "sociowire-frontend",
- "version": "0.0.102",
+ "version": "0.0.104",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sociowire-frontend",
- "version": "0.0.102",
+ "version": "0.0.104",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0",
"axios": "^1.13.2",
diff --git a/package.json b/package.json
index 5e668e1..5d0c769 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "sociowire-frontend",
"private": true,
- "version": "0.0.103",
+ "version": "0.0.104",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
diff --git a/src/App.jsx b/src/App.jsx
index d04fa3d..f50cbde 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -4,23 +4,29 @@ import { version as appVersion } from "../package.json";
import TopBar from "./components/Layout/TopBarNew";
import PostList from "./components/Posts/PostList";
import ContentFilters from "./components/Filters/ContentFilters";
-import IslandUniverse from "./components/Island/IslandUniverse";
import IslandViewer from "./components/Island/IslandViewer";
import ChatContainer from "./components/Chat/ChatContainer";
import PostDetailModal from "./components/Posts/PostDetailModal";
import { CreatePostFAB } from "./components/Posts/PostComposer";
-import { AnimatePresence } from "framer-motion";
+import { AnimatePresence, motion } from "framer-motion";
import { fetchIslandByUsername } from "./api/client";
const MapView = React.lazy(() => import("./components/Map/MapView"));
+const IslandsWorld = React.lazy(() => import("./components/World/IslandsWorld"));
+const PlanetsWorld = React.lazy(() => import("./components/World/PlanetsWorld"));
const THEME_KEY = "sociowire:theme";
const CONTENT_FILTER_KEY = "sociowire:contentFilter";
+const VIEW_MODE_KEY = "sociowire:viewMode"; // "map" | "islands" | "planets"
export default function App() {
const { t } = useTranslation();
const [theme, setTheme] = useState("blue");
+ // Current world view: "map" | "islands" | "planets"
+ // Always start on map for now
+ const [currentView, setCurrentView] = useState("map");
+
// Clean up Facebook OAuth hash fragment (#_=_)
useEffect(() => {
if (window.location.hash === "#_=_") {
@@ -57,8 +63,7 @@ export default function App() {
} catch {}
}, [contentFilter]);
- // Island Universe & Island Viewer state
- const [showUniverse, setShowUniverse] = useState(false);
+ // Island Viewer state (for viewing individual islands)
const [selectedIsland, setSelectedIsland] = useState(null);
// Post detail modal state (triggered from map markers)
@@ -159,17 +164,20 @@ export default function App() {
return () => window.removeEventListener('sociowire:create-post', handleIslandPost);
}, []);
- // Island Universe handlers
- const handleOpenUniverse = () => {
- setShowUniverse(true);
+ // World navigation handlers
+ const handleOpenIslands = () => {
+ setCurrentView("islands");
};
- const handleCloseUniverse = () => {
- setShowUniverse(false);
+ const handleOpenPlanets = () => {
+ setCurrentView("planets");
+ };
+
+ const handleOpenMap = () => {
+ setCurrentView("map");
};
const handleIslandClick = (island) => {
- setShowUniverse(false); // Close universe
setSelectedIsland(island); // Open island viewer
};
@@ -179,134 +187,174 @@ export default function App() {
try {
const island = await fetchIslandByUsername(u);
if (island) {
- setShowUniverse(false);
setSelectedIsland(island);
}
} catch {}
};
+ // World loading placeholder
+ const WorldPlaceholder = ({ title, subtitle }) => (
+
+ );
+
return (
+ {/* World Switcher Navigation */}
+
+
+
+
+
+
-
-
-
-
-
{t('map.loadingMap')}
-
{t('map.warmingUp')}
-
-
-
-
-
-
-
-
+
+ {/* MAP WORLD - Reality */}
+ {currentView === "map" && (
+
+
+ }>
+
+
+
+
+ {/* BELOW THE MAP (page scroll like Facebook) */}
+
- }
- >
-
-
-
+
+ )}
- {/* BELOW THE MAP (page scroll like Facebook) */}
-
- {/* Content filters - shared between map and sociowall */}
-
-
-
- {/* SocioWall header inside panel */}
-
-
SOCIOWALL
-
-
-
-
-
+ {/* ISLANDS WORLD - Personal Metaverse */}
+ {currentView === "islands" && (
+
+ }>
+
+
+
+ )}
-
+ {/* PLANETS WORLD - Group Metaverse */}
+ {currentView === "planets" && (
+
+ }>
+
+
+
+ )}
+
-
-
-
{t('islands.communities')}
-
{t('islands.exploreIslands')}
-
- {t('islands.islandsDescription')}
-
-
-
-
-
- {/* Chat System */}
+ {/* Chat System - available in all worlds */}
-
-
- {/* Island Universe Modal */}
- {showUniverse && (
-
+ {/* Footer only shows in map view */}
+ {currentView === "map" && (
+
)}
{/* Island Viewer Modal */}
diff --git a/src/components/Island/IslandUniverse.jsx b/src/components/Island/IslandUniverse.jsx
index f275016..39835e4 100644
--- a/src/components/Island/IslandUniverse.jsx
+++ b/src/components/Island/IslandUniverse.jsx
@@ -93,7 +93,7 @@ export default function IslandUniverse({ onClose, onIslandClick }) {
// Use grid-based positioning to prevent overlap
const gridSize = Math.ceil(Math.sqrt(displayIslands.length || 1));
- const spacing = 8; // degrees between islands
+ const spacing = 15; // degrees between islands (increased for better separation)
displayIslands.forEach((island, idx) => {
// Grid position with random offset for organic feel
@@ -101,9 +101,9 @@ export default function IslandUniverse({ onClose, onIslandClick }) {
const gridY = Math.floor(idx / gridSize);
const baseX = (gridX - gridSize / 2) * spacing;
const baseY = (gridY - gridSize / 2) * spacing;
- // Add controlled randomness within cell
- const jitterX = (Math.random() - 0.5) * spacing * 0.6;
- const jitterY = (Math.random() - 0.5) * spacing * 0.6;
+ // Add controlled randomness within cell (reduced jitter to prevent overlap)
+ const jitterX = (Math.random() - 0.5) * spacing * 0.3;
+ const jitterY = (Math.random() - 0.5) * spacing * 0.3;
const lng = centerLng + baseX + jitterX;
const lat = centerLat + baseY + jitterY;
@@ -189,10 +189,10 @@ export default function IslandUniverse({ onClose, onIslandClick }) {
markers.push({ marker, island, el });
});
- // Add planet markers (positioned in outer ring)
+ // Add planet markers (positioned in outer ring with more space)
displayPlanets.forEach((planet, idx) => {
const angle = (idx / Math.max(planets.length, 1)) * Math.PI * 2;
- const radius = 18 + (idx % 3) * 4;
+ const radius = 30 + (idx % 3) * 8; // increased radius for better separation
const lng = centerLng + Math.cos(angle) * radius;
const lat = centerLat + Math.sin(angle) * radius;
diff --git a/src/components/Layout/TopBarNew.jsx b/src/components/Layout/TopBarNew.jsx
index c422bee..73e9cbd 100644
--- a/src/components/Layout/TopBarNew.jsx
+++ b/src/components/Layout/TopBarNew.jsx
@@ -235,7 +235,7 @@ function UserChip({ authenticated, loading, username, avatarUrl, onClick }) {
// Main Component
// ─────────────────────────────────────────────────────────────────────────────
-export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland, onMyLocation, onSearch }) {
+export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onPlanetsClick, onMapClick, onUserIsland, onMyLocation, onSearch, currentView }) {
const { t, i18n } = useTranslation();
const {
loading, authenticated, username, login, logout, restore, loginWithProvider, ssoEnabled,
diff --git a/src/components/World/IslandsWorld.css b/src/components/World/IslandsWorld.css
new file mode 100644
index 0000000..d173f88
--- /dev/null
+++ b/src/components/World/IslandsWorld.css
@@ -0,0 +1,335 @@
+/* Islands World - Full screen metaverse */
+.islands-world {
+ position: relative;
+ width: 100%;
+ min-height: 100vh;
+ background: linear-gradient(180deg, #0a1628 0%, #0d2847 50%, #1e3a5f 100%);
+ overflow: hidden;
+}
+
+.islands-world-canvas {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+/* Search bar */
+.islands-world-search {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: rgba(15, 23, 42, 0.9);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(56, 189, 248, 0.3);
+ border-radius: 50px;
+ padding: 10px 16px;
+ width: 90%;
+ max-width: 360px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+}
+
+.islands-world-search .search-icon {
+ color: #38bdf8;
+ font-size: 18px;
+}
+
+.islands-world-search i {
+ color: rgba(255, 255, 255, 0.5);
+ font-size: 14px;
+}
+
+.islands-world-search .search-count {
+ font-size: 11px;
+ color: rgba(56, 189, 248, 0.8);
+ font-weight: 600;
+ padding: 2px 8px;
+ background: rgba(56, 189, 248, 0.15);
+ border-radius: 20px;
+}
+
+.islands-world-search input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: white;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.islands-world-search input::placeholder {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.islands-world-search .search-clear {
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ border-radius: 50%;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.islands-world-search .search-clear:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+/* Island markers */
+.island-marker {
+ cursor: pointer;
+ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.island-marker:hover {
+ transform: scale(1.15) translateY(-8px);
+ z-index: 100;
+}
+
+.island-marker-inner {
+ position: relative;
+ animation: islandFloat 6s ease-in-out infinite;
+ filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.6));
+}
+
+.island-marker:hover .island-marker-inner {
+ filter: drop-shadow(0 12px 32px rgba(56, 189, 248, 0.4));
+}
+
+.island-svg {
+ overflow: visible;
+}
+
+.island-marker-label {
+ position: absolute;
+ bottom: -30px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(30, 41, 59, 0.9));
+ padding: 6px 14px;
+ border-radius: 16px;
+ border: 1px solid rgba(56, 189, 248, 0.3);
+ white-space: nowrap;
+ text-align: center;
+ backdrop-filter: blur(8px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
+}
+
+.island-marker-name {
+ display: block;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.island-marker-meta {
+ display: block;
+ font-size: 9px;
+ color: rgba(255, 255, 255, 0.6);
+ margin-top: 2px;
+}
+
+/* Tooltip */
+.islands-world-tooltip {
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 200;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.95));
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(56, 189, 248, 0.4);
+ border-radius: 20px;
+ padding: 16px 24px;
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
+ animation: tooltipSlide 0.3s ease;
+}
+
+@keyframes tooltipSlide {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+.tooltip-avatar {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ overflow: hidden;
+ background: linear-gradient(135deg, #38bdf8, #06b6d4);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ box-shadow: 0 4px 16px rgba(56, 189, 248, 0.4);
+}
+
+.tooltip-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.tooltip-avatar span {
+ font-size: 24px;
+ font-weight: 700;
+ color: white;
+}
+
+.tooltip-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.tooltip-username {
+ font-size: 16px;
+ font-weight: 700;
+ color: #38bdf8;
+}
+
+.tooltip-bio {
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.8);
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tooltip-stats {
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.5);
+ font-weight: 600;
+}
+
+/* Create island button */
+.islands-world-create {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ z-index: 200;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: linear-gradient(135deg, #10b981, #059669);
+ border: none;
+ border-radius: 50px;
+ padding: 14px 24px;
+ color: white;
+ font-size: 14px;
+ font-weight: 700;
+ cursor: pointer;
+ box-shadow: 0 8px 32px rgba(16, 185, 129, 0.4);
+ transition: all 0.3s ease;
+}
+
+.islands-world-create:hover {
+ transform: scale(1.05);
+ box-shadow: 0 12px 40px rgba(16, 185, 129, 0.5);
+}
+
+.islands-world-create i {
+ font-size: 16px;
+}
+
+/* Loading overlay */
+.islands-world-loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+ background: rgba(10, 22, 40, 0.95);
+ z-index: 300;
+}
+
+.loading-spinner {
+ width: 60px;
+ height: 60px;
+ border: 4px solid rgba(56, 189, 248, 0.2);
+ border-top-color: #38bdf8;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.islands-world-loading p {
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 16px;
+ font-weight: 600;
+}
+
+/* Float animation */
+@keyframes islandFloat {
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-12px);
+ }
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .islands-world-title {
+ font-size: 22px;
+ }
+
+ .islands-world-title i {
+ font-size: 24px;
+ }
+
+ .islands-world-search {
+ top: 90px;
+ padding: 10px 16px;
+ }
+
+ .islands-world-tooltip {
+ bottom: 70px;
+ padding: 12px 16px;
+ gap: 12px;
+ }
+
+ .tooltip-avatar {
+ width: 44px;
+ height: 44px;
+ }
+
+ .tooltip-avatar span {
+ font-size: 18px;
+ }
+
+ .tooltip-username {
+ font-size: 14px;
+ }
+
+ .islands-world-create {
+ bottom: 16px;
+ right: 16px;
+ padding: 12px 18px;
+ font-size: 13px;
+ }
+}
diff --git a/src/components/World/IslandsWorld.jsx b/src/components/World/IslandsWorld.jsx
new file mode 100644
index 0000000..1fe1476
--- /dev/null
+++ b/src/components/World/IslandsWorld.jsx
@@ -0,0 +1,408 @@
+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 }) {
+ 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);
+
+ el.addEventListener('mouseenter', () => setHoveredIsland(island));
+ el.addEventListener('mouseleave', () => setHoveredIsland(null));
+ el.addEventListener('click', (e) => {
+ e.stopPropagation();
+ 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 (
+
+ {/* Search bar with icon */}
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+ {searchQuery && (
+
+ )}
+ {islands.length}
+
+
+ {/* Main canvas */}
+
+
+ {/* Hovered island tooltip */}
+ {hoveredIsland && (
+
+
+ {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'}
+
+
+
+ )}
+
+ {/* Create Island CTA (for authenticated users without island) */}
+ {authenticated && (
+
+ )}
+
+ {/* Loading overlay */}
+ {loading && (
+
+
+
{t('worlds.loadingIslands')}
+
+ )}
+
+ );
+}
+
+/**
+ * Create a stylized island marker element
+ */
+function createIslandMarker(island, idx, t) {
+ 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;
+
+ // Generate unique island shape
+ const islandSVG = generateIslandSVG(seed, color);
+ const postCount = island.content_count || 0;
+
+ el.innerHTML = `
+
+ ${islandSVG}
+
+ @${island.username}
+ ${postCount} posts
+
+
+ `;
+
+ 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;
+
+ // 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
+ });
+ }
+
+ // Create smooth path
+ 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';
+
+ // Add decorations
+ 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 `
+
+ `;
+}
+
+/**
+ * Create starfield GeoJSON
+ */
+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 };
+}
+
+/**
+ * Create ocean gradient GeoJSON
+ */
+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 };
+}
+
+/**
+ * Seeded random number generator
+ */
+function seededRandom(seed) {
+ let s = seed;
+ return function() {
+ s = (s * 9301 + 49297) % 233280;
+ return s / 233280;
+ };
+}
diff --git a/src/components/World/PlanetsWorld.css b/src/components/World/PlanetsWorld.css
new file mode 100644
index 0000000..b3c3c90
--- /dev/null
+++ b/src/components/World/PlanetsWorld.css
@@ -0,0 +1,498 @@
+/* Planets World - Full screen group metaverse */
+.planets-world {
+ position: relative;
+ width: 100%;
+ min-height: 100vh;
+ background: radial-gradient(ellipse at center, #0f0f1a 0%, #050510 70%, #000005 100%);
+ overflow: hidden;
+}
+
+.planets-world-canvas {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+/* Search bar */
+.planets-world-search {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: rgba(15, 15, 26, 0.9);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(155, 89, 182, 0.3);
+ border-radius: 50px;
+ padding: 10px 16px;
+ width: 90%;
+ max-width: 360px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
+}
+
+.planets-world-search .search-icon {
+ color: #9b59b6;
+ font-size: 18px;
+}
+
+.planets-world-search i {
+ color: rgba(255, 255, 255, 0.5);
+ font-size: 14px;
+}
+
+.planets-world-search .search-count {
+ font-size: 11px;
+ color: rgba(155, 89, 182, 0.9);
+ font-weight: 600;
+ padding: 2px 8px;
+ background: rgba(155, 89, 182, 0.15);
+ border-radius: 20px;
+}
+
+.planets-world-search input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: white;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.planets-world-search input::placeholder {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.planets-world-search .search-clear {
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ border-radius: 50%;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.planets-world-search .search-clear:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+/* Planet markers */
+.planet-marker {
+ cursor: pointer;
+ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.planet-marker:hover {
+ transform: scale(1.2);
+ z-index: 100;
+}
+
+.planet-marker-inner {
+ position: relative;
+ animation: planetOrbit 12s ease-in-out infinite;
+ filter: drop-shadow(0 0 20px rgba(155, 89, 182, 0.4));
+}
+
+.planet-marker:hover .planet-marker-inner {
+ filter: drop-shadow(0 0 30px rgba(155, 89, 182, 0.7));
+}
+
+.planet-svg {
+ overflow: visible;
+}
+
+.planet-marker-label {
+ position: absolute;
+ bottom: -25px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: linear-gradient(135deg, rgba(15, 15, 26, 0.95), rgba(30, 30, 50, 0.9));
+ padding: 6px 14px;
+ border-radius: 16px;
+ border: 1px solid rgba(155, 89, 182, 0.3);
+ white-space: nowrap;
+ text-align: center;
+ backdrop-filter: blur(8px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
+}
+
+.planet-marker-name {
+ display: block;
+ font-size: 12px;
+ font-weight: 700;
+ color: #d4a5ff;
+}
+
+.planet-marker-meta {
+ display: block;
+ font-size: 9px;
+ color: rgba(255, 255, 255, 0.6);
+ margin-top: 2px;
+}
+
+/* Tooltip */
+.planets-world-tooltip {
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 200;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ background: linear-gradient(135deg, rgba(15, 15, 26, 0.98), rgba(30, 30, 50, 0.95));
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(155, 89, 182, 0.4);
+ border-radius: 20px;
+ padding: 16px 24px;
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6);
+ animation: tooltipSlide 0.3s ease;
+}
+
+@keyframes tooltipSlide {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+.tooltip-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ box-shadow: 0 4px 16px rgba(155, 89, 182, 0.4);
+}
+
+.tooltip-icon i {
+ font-size: 24px;
+ color: white;
+}
+
+.tooltip-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.tooltip-name {
+ font-size: 16px;
+ font-weight: 700;
+ color: #d4a5ff;
+}
+
+.tooltip-desc {
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.8);
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tooltip-stats {
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.5);
+ font-weight: 600;
+}
+
+/* Planet detail modal */
+.planet-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(10px);
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.planet-modal {
+ width: 90%;
+ max-width: 480px;
+ background: linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%);
+ border: 1px solid rgba(155, 89, 182, 0.3);
+ border-radius: 24px;
+ overflow: hidden;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
+ animation: modalSlide 0.3s ease;
+}
+
+@keyframes modalSlide {
+ from {
+ opacity: 0;
+ transform: scale(0.9) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.planet-modal-close {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ width: 36px;
+ height: 36px;
+ border: none;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ font-size: 16px;
+ cursor: pointer;
+ z-index: 10;
+ transition: background 0.2s;
+}
+
+.planet-modal-close:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.planet-modal-header {
+ position: relative;
+ padding: 40px 24px 24px;
+ text-align: center;
+}
+
+.planet-modal-icon {
+ width: 80px;
+ height: 80px;
+ margin: 0 auto 16px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.15);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+}
+
+.planet-modal-icon i {
+ font-size: 36px;
+ color: white;
+}
+
+.planet-modal-header h2 {
+ font-size: 24px;
+ font-weight: 800;
+ color: white;
+ margin: 0 0 8px;
+}
+
+.planet-modal-header p {
+ font-size: 14px;
+ color: rgba(255, 255, 255, 0.8);
+ margin: 0;
+}
+
+.planet-modal-stats {
+ display: flex;
+ justify-content: center;
+ gap: 32px;
+ padding: 24px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.planet-modal-stats .stat {
+ text-align: center;
+}
+
+.planet-modal-stats .stat-value {
+ display: block;
+ font-size: 24px;
+ font-weight: 800;
+ color: #d4a5ff;
+}
+
+.planet-modal-stats .stat-label {
+ display: block;
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.5);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-top: 4px;
+}
+
+.planet-modal-actions {
+ display: flex;
+ gap: 12px;
+ padding: 24px;
+}
+
+.planet-action-btn {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 14px 20px;
+ border: none;
+ border-radius: 12px;
+ font-size: 14px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+}
+
+.planet-action-btn:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.planet-action-btn.planet-action-primary {
+ background: linear-gradient(135deg, #9b59b6, #8e44ad);
+ box-shadow: 0 4px 16px rgba(155, 89, 182, 0.4);
+}
+
+.planet-action-btn.planet-action-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(155, 89, 182, 0.5);
+}
+
+/* Create Planet button */
+.planets-world-create {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ z-index: 200;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: linear-gradient(135deg, #9b59b6, #8e44ad);
+ border: none;
+ border-radius: 50px;
+ padding: 14px 24px;
+ color: white;
+ font-size: 14px;
+ font-weight: 700;
+ cursor: pointer;
+ box-shadow: 0 8px 32px rgba(155, 89, 182, 0.4);
+ transition: all 0.3s ease;
+}
+
+.planets-world-create:hover {
+ transform: scale(1.05);
+ box-shadow: 0 12px 40px rgba(155, 89, 182, 0.5);
+}
+
+.planets-world-create i {
+ font-size: 16px;
+}
+
+/* Loading overlay */
+.planets-world-loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+ background: rgba(5, 5, 16, 0.95);
+ z-index: 300;
+}
+
+.planets-world-loading .loading-spinner {
+ width: 60px;
+ height: 60px;
+ border: 4px solid rgba(155, 89, 182, 0.2);
+ border-top-color: #9b59b6;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.planets-world-loading p {
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 16px;
+ font-weight: 600;
+}
+
+/* Orbit animation */
+@keyframes planetOrbit {
+ 0%, 100% {
+ transform: translateY(0) rotate(0deg);
+ }
+ 25% {
+ transform: translateY(-8px) rotate(2deg);
+ }
+ 50% {
+ transform: translateY(0) rotate(0deg);
+ }
+ 75% {
+ transform: translateY(8px) rotate(-2deg);
+ }
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .planets-world-title {
+ font-size: 22px;
+ }
+
+ .planets-world-title i {
+ font-size: 24px;
+ }
+
+ .planets-world-search {
+ top: 90px;
+ padding: 10px 16px;
+ }
+
+ .planets-world-tooltip {
+ bottom: 70px;
+ padding: 12px 16px;
+ gap: 12px;
+ }
+
+ .tooltip-icon {
+ width: 44px;
+ height: 44px;
+ }
+
+ .tooltip-icon i {
+ font-size: 18px;
+ }
+
+ .tooltip-name {
+ font-size: 14px;
+ }
+
+ .planets-world-create {
+ bottom: 16px;
+ right: 16px;
+ padding: 12px 18px;
+ font-size: 13px;
+ }
+
+ .planet-modal {
+ width: 95%;
+ }
+}
diff --git a/src/components/World/PlanetsWorld.jsx b/src/components/World/PlanetsWorld.jsx
new file mode 100644
index 0000000..ec97e8b
--- /dev/null
+++ b/src/components/World/PlanetsWorld.jsx
@@ -0,0 +1,392 @@
+import React, { useEffect, useRef, useState } 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 { 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 }) {
+ const { t } = useTranslation();
+ const { authenticated } = useAuth();
+ const containerRef = useRef(null);
+ const mapRef = useRef(null);
+ const markersRef = useRef([]);
+ const animationRef = useRef(null);
+
+ const [planets, setPlanets] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [hoveredPlanet, setHoveredPlanet] = useState(null);
+ const [selectedPlanet, setSelectedPlanet] = useState(null);
+
+ // Fetch planets
+ useEffect(() => {
+ let mounted = true;
+ setLoading(true);
+
+ fetchPlanets({ limit: 100 })
+ .then(data => {
+ if (mounted) {
+ setPlanets(data || []);
+ setLoading(false);
+ }
+ })
+ .catch(err => {
+ console.warn('[PlanetsWorld] Failed to fetch planets:', err);
+ if (mounted) setLoading(false);
+ });
+
+ return () => { mounted = false; };
+ }, []);
+
+ // Filter planets by search
+ const filteredPlanets = searchQuery
+ ? planets.filter(p =>
+ (p.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (p.description || '').toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ : planets;
+
+ // 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
+ }
+ }
+ ]
+ },
+ center: [centerLng, centerLat],
+ zoom: 0.8,
+ pitch: 45,
+ bearing: 0,
+ antialias: true,
+ attributionControl: false
+ });
+
+ mapRef.current = map;
+
+ map.on('load', () => {
+ // Add 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']
+ }
+ });
+
+ // Add starfield
+ const starsData = createStarfield(800);
+ map.addSource('stars', { type: 'geojson', data: starsData });
+ map.addLayer({
+ id: 'stars',
+ type: 'circle',
+ source: 'stars',
+ paint: {
+ 'circle-radius': ['interpolate', ['linear'], ['zoom'], 0, 0.8, 5, 2.5],
+ 'circle-color': ['get', 'color'],
+ 'circle-opacity': ['get', 'opacity'],
+ 'circle-blur': 0.2
+ }
+ });
+
+ // Create planet markers in orbital rings
+ const markers = [];
+ const ringCount = Math.ceil(filteredPlanets.length / 6);
+
+ 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 = centerLng + Math.cos(angle) * baseRadius;
+ const lat = centerLat + Math.sin(angle) * baseRadius;
+
+ const el = createPlanetMarker(planet, idx, t);
+
+ el.addEventListener('mouseenter', () => setHoveredPlanet(planet));
+ el.addEventListener('mouseleave', () => setHoveredPlanet(null));
+ el.addEventListener('click', (e) => {
+ e.stopPropagation();
+ setSelectedPlanet(planet);
+ });
+
+ const marker = new maplibregl.Marker({
+ element: el,
+ anchor: 'center'
+ })
+ .setLngLat([lng, lat])
+ .addTo(map);
+
+ markers.push({ marker, planet, el });
+ });
+
+ markersRef.current = markers;
+
+ // Slow cosmic rotation
+ let bearing = 0;
+ function animate() {
+ bearing += 0.03;
+ 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();
+ };
+ }, [filteredPlanets, loading, t]);
+
+ return (
+
+ {/* Search bar with icon */}
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+ {searchQuery && (
+
+ )}
+ {planets.length}
+
+
+ {/* Main canvas */}
+
+
+ {/* Hovered planet tooltip */}
+ {hoveredPlanet && (
+
+
+
+
+
+
{hoveredPlanet.name || 'Unknown Planet'}
+
{hoveredPlanet.description || t('worlds.clickToExplore')}
+
+ {hoveredPlanet.member_count || 0} {t('worlds.members')} • {hoveredPlanet.theme || 'default'}
+
+
+
+ )}
+
+ {/* Planet detail modal */}
+ {selectedPlanet && (
+
setSelectedPlanet(null)}>
+
e.stopPropagation()}>
+
+
+
+
+
+
+
{selectedPlanet.name}
+
{selectedPlanet.description || t('worlds.noDescription')}
+
+
+
+
+ {selectedPlanet.member_count || 0}
+ {t('worlds.members')}
+
+
+ {selectedPlanet.post_count || 0}
+ {t('worlds.posts')}
+
+
+ {selectedPlanet.theme || 'default'}
+ {t('worlds.theme')}
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Create Planet CTA */}
+ {authenticated && (
+
+ )}
+
+ {/* Loading overlay */}
+ {loading && (
+
+
+
{t('worlds.loadingPlanets')}
+
+ )}
+
+ );
+}
+
+/**
+ * Create a stylized planet marker element
+ */
+function createPlanetMarker(planet, idx, t) {
+ const el = document.createElement('div');
+ el.className = 'planet-marker';
+
+ const themeColors = {
+ 'default': ['#9b59b6', '#8e44ad'],
+ 'tech': ['#3498db', '#2980b9'],
+ 'gaming': ['#e74c3c', '#c0392b'],
+ 'music': ['#1abc9c', '#16a085'],
+ 'art': ['#e91e63', '#c2185b'],
+ 'science': ['#00bcd4', '#0097a7'],
+ 'sports': ['#ff9800', '#f57c00'],
+ };
+ const colors = themeColors[planet.theme] || themeColors['default'];
+ const memberCount = planet.member_count || 0;
+
+ el.innerHTML = `
+
+
+
+ ${planet.name || 'Planet'}
+ ${memberCount} members
+
+
+ `;
+
+ return el;
+}
+
+/**
+ * Get planet gradient for backgrounds
+ */
+function getPlanetGradient(theme) {
+ const gradients = {
+ 'default': 'linear-gradient(135deg, #9b59b6, #8e44ad)',
+ 'tech': 'linear-gradient(135deg, #3498db, #2980b9)',
+ 'gaming': 'linear-gradient(135deg, #e74c3c, #c0392b)',
+ 'music': 'linear-gradient(135deg, #1abc9c, #16a085)',
+ 'art': 'linear-gradient(135deg, #e91e63, #c2185b)',
+ 'science': 'linear-gradient(135deg, #00bcd4, #0097a7)',
+ 'sports': 'linear-gradient(135deg, #ff9800, #f57c00)',
+ };
+ return gradients[theme] || gradients['default'];
+}
+
+/**
+ * Create starfield GeoJSON
+ */
+function createStarfield(count) {
+ const features = [];
+ for (let i = 0; i < count; i++) {
+ const lng = (Math.random() - 0.5) * 500;
+ const lat = (Math.random() - 0.5) * 500;
+ const brightness = Math.random();
+ features.push({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [lng, lat] },
+ properties: {
+ color: brightness > 0.85 ? '#ffeedd' : brightness > 0.6 ? '#aaccff' : '#ffffff',
+ opacity: 0.2 + brightness * 0.6
+ }
+ });
+ }
+ return { type: 'FeatureCollection', features };
+}
+
+/**
+ * Create nebula effect GeoJSON
+ */
+function createNebulaEffect() {
+ const features = [];
+ const nebulaColors = [
+ { color: 'rgba(155, 89, 182, 0.15)', x: -50, y: 30 },
+ { color: 'rgba(52, 152, 219, 0.12)', x: 60, y: -40 },
+ { color: 'rgba(231, 76, 60, 0.1)', x: -30, y: -60 },
+ { color: 'rgba(26, 188, 156, 0.08)', x: 80, y: 50 },
+ ];
+
+ nebulaColors.forEach((nebula, i) => {
+ const coords = [];
+ const numPoints = 32;
+ for (let j = 0; j <= numPoints; j++) {
+ const angle = (j / numPoints) * Math.PI * 2;
+ const radius = 60 + Math.random() * 30;
+ coords.push([
+ nebula.x + Math.cos(angle) * radius,
+ nebula.y + Math.sin(angle) * radius
+ ]);
+ }
+ features.push({
+ type: 'Feature',
+ geometry: { type: 'Polygon', coordinates: [coords] },
+ properties: { color: nebula.color, opacity: 1 }
+ });
+ });
+
+ return { type: 'FeatureCollection', features };
+}
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index d9836b5..da4147b 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -277,6 +277,30 @@
"planetsTab": "Planets",
"members": "members"
},
+ "worlds": {
+ "map": "Reality",
+ "islands": "Islands",
+ "planets": "Planets",
+ "islandsTitle": "Island Metaverse",
+ "islandsSubtitle": "{{count}} personal islands floating in virtual space",
+ "planetsTitle": "Planet Metaverse",
+ "planetsSubtitle": "{{count}} community planets orbiting in space",
+ "loadingIslands": "Loading Islands",
+ "loadingPlanets": "Loading Planets",
+ "enteringMetaverse": "Entering the metaverse...",
+ "searchIslands": "Search islands by username...",
+ "searchPlanets": "Search planets by name...",
+ "clickToVisit": "Click to visit",
+ "clickToExplore": "Click to explore",
+ "posts": "posts",
+ "members": "members",
+ "theme": "Theme",
+ "createIsland": "Create My Island",
+ "createPlanet": "Create Planet",
+ "joinPlanet": "Join Planet",
+ "share": "Share",
+ "noDescription": "No description yet"
+ },
"controls": {
"mySpot": "My spot",
"putWire": "Put a wire"
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index aa74744..40186a7 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -277,6 +277,30 @@
"planetsTab": "Planetas",
"members": "miembros"
},
+ "worlds": {
+ "map": "Realidad",
+ "islands": "Islas",
+ "planets": "Planetas",
+ "islandsTitle": "Metaverso de Islas",
+ "islandsSubtitle": "{{count}} islas personales flotando en el espacio virtual",
+ "planetsTitle": "Metaverso de Planetas",
+ "planetsSubtitle": "{{count}} planetas comunitarios orbitando en el espacio",
+ "loadingIslands": "Cargando Islas",
+ "loadingPlanets": "Cargando Planetas",
+ "enteringMetaverse": "Entrando al metaverso...",
+ "searchIslands": "Buscar islas por nombre de usuario...",
+ "searchPlanets": "Buscar planetas por nombre...",
+ "clickToVisit": "Clic para visitar",
+ "clickToExplore": "Clic para explorar",
+ "posts": "posts",
+ "members": "miembros",
+ "theme": "Tema",
+ "createIsland": "Crear mi Isla",
+ "createPlanet": "Crear Planeta",
+ "joinPlanet": "Unirse al Planeta",
+ "share": "Compartir",
+ "noDescription": "Sin descripción"
+ },
"controls": {
"mySpot": "Mi ubicación",
"putWire": "Publicar un wire"
diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json
index d371bfe..ec54893 100644
--- a/src/locales/fr/translation.json
+++ b/src/locales/fr/translation.json
@@ -280,6 +280,30 @@
"planetsTab": "Planètes",
"members": "membres"
},
+ "worlds": {
+ "map": "Réalité",
+ "islands": "Îles",
+ "planets": "Planètes",
+ "islandsTitle": "Métaverse des Îles",
+ "islandsSubtitle": "{{count}} îles personnelles flottant dans l'espace virtuel",
+ "planetsTitle": "Métaverse des Planètes",
+ "planetsSubtitle": "{{count}} planètes communautaires en orbite dans l'espace",
+ "loadingIslands": "Chargement des Îles",
+ "loadingPlanets": "Chargement des Planètes",
+ "enteringMetaverse": "Entrée dans le métaverse...",
+ "searchIslands": "Rechercher des îles par nom d'utilisateur...",
+ "searchPlanets": "Rechercher des planètes par nom...",
+ "clickToVisit": "Cliquez pour visiter",
+ "clickToExplore": "Cliquez pour explorer",
+ "posts": "posts",
+ "members": "membres",
+ "theme": "Thème",
+ "createIsland": "Créer mon Île",
+ "createPlanet": "Créer une Planète",
+ "joinPlanet": "Rejoindre la Planète",
+ "share": "Partager",
+ "noDescription": "Pas de description"
+ },
"controls": {
"mySpot": "Ma position",
"putWire": "Poster un wire"
diff --git a/src/styles/layout.css b/src/styles/layout.css
index 0276348..df067b8 100644
--- a/src/styles/layout.css
+++ b/src/styles/layout.css
@@ -154,3 +154,111 @@
/* Espace pour les bulles */
.below-stage { padding-bottom: 70px; }
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ World Navigation & Containers
+ ───────────────────────────────────────────────────────────────────────────── */
+
+.world-nav {
+ position: sticky;
+ top: 52px;
+ z-index: 900;
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding: 8px 16px;
+ background: rgba(2, 8, 23, 0.95);
+ backdrop-filter: blur(20px);
+}
+
+.world-nav-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 20px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 50px;
+ background: rgba(255, 255, 255, 0.05);
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.world-nav-btn i {
+ font-size: 14px;
+}
+
+.world-nav-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+.world-nav-btn.is-active {
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(6, 182, 212, 0.3));
+ border-color: rgba(59, 130, 246, 0.5);
+ color: white;
+ box-shadow: 0 4px 20px rgba(59, 130, 246, 0.25);
+}
+
+.world-nav-btn.is-active i {
+ color: #38bdf8;
+}
+
+/* World containers */
+.world-container {
+ width: 100%;
+}
+
+.world-fullscreen {
+ min-height: calc(100vh - 110px);
+}
+
+/* World placeholder */
+.world-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 60vh;
+ background: linear-gradient(180deg, #0a1628 0%, #0d2847 100%);
+}
+
+.world-placeholder-content {
+ text-align: center;
+}
+
+.world-placeholder-title {
+ font-size: 24px;
+ font-weight: 700;
+ color: white;
+ margin-bottom: 8px;
+}
+
+.world-placeholder-subtitle {
+ font-size: 14px;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* Responsive */
+@media (max-width: 640px) {
+ .world-nav {
+ gap: 6px;
+ padding: 8px 12px;
+ }
+
+ .world-nav-btn {
+ padding: 8px 14px;
+ font-size: 12px;
+ }
+
+ .world-nav-btn span {
+ display: none;
+ }
+
+ .world-nav-btn i {
+ font-size: 16px;
+ }
+}