From 9706d673c3e795a0a6deff20d94c6f3b753f0a44 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 24 Jan 2026 19:53:32 +0000 Subject: [PATCH] feat: Add 3 separate worlds system (Map, Islands, Planets) - Add world navigation bar with 3 buttons to switch between worlds - Create IslandsWorld component: personal metaverse with floating islands - Create PlanetsWorld component: group metaverse with orbiting planets - Each world has full-screen immersive MapLibre visualization - Add search functionality with icon badges in Islands/Planets views - Always start on Map view on page load - Add translations for EN, FR, ES - v0.0.104 Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 4 +- package.json | 2 +- src/App.jsx | 278 +++++++------ src/components/Island/IslandUniverse.jsx | 12 +- src/components/Layout/TopBarNew.jsx | 2 +- src/components/World/IslandsWorld.css | 335 +++++++++++++++ src/components/World/IslandsWorld.jsx | 408 +++++++++++++++++++ src/components/World/PlanetsWorld.css | 498 +++++++++++++++++++++++ src/components/World/PlanetsWorld.jsx | 392 ++++++++++++++++++ src/locales/en/translation.json | 24 ++ src/locales/es/translation.json | 24 ++ src/locales/fr/translation.json | 24 ++ src/styles/layout.css | 108 +++++ 13 files changed, 1986 insertions(+), 125 deletions(-) create mode 100644 src/components/World/IslandsWorld.css create mode 100644 src/components/World/IslandsWorld.jsx create mode 100644 src/components/World/PlanetsWorld.css create mode 100644 src/components/World/PlanetsWorld.jsx 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 }) => ( +
+
+
{title}
+
{subtitle}
+
+
+ ); + return (
+ {/* World Switcher Navigation */} +
+ + + +
+
-
- -
-
-
{t('map.loadingMap')}
-
{t('map.warmingUp')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + {/* MAP WORLD - Reality */} + {currentView === "map" && ( + +
+ }> + + +
+ + {/* BELOW THE MAP (page scroll like Facebook) */} +
+ +
+
+
+

SOCIOWALL

+
+
- } - > - - -
+ + )} - {/* 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} + ) : ( + {(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 ` + + + + + + + + + + + + + + + + + + + + + ${trees} + + `; +} + +/** + * 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; + } +}