diff --git a/package-lock.json b/package-lock.json
index d3ef83a..0b865e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "sociowire-frontend",
- "version": "0.0.109",
+ "version": "0.0.110",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sociowire-frontend",
- "version": "0.0.109",
+ "version": "0.0.110",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0",
"axios": "^1.13.2",
diff --git a/package.json b/package.json
index af3f260..fc98810 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "sociowire-frontend",
"private": true,
- "version": "0.0.109",
+ "version": "0.0.110",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
diff --git a/src/api/client.js b/src/api/client.js
index 2005ef5..e6634e4 100644
--- a/src/api/client.js
+++ b/src/api/client.js
@@ -677,6 +677,19 @@ export async function fetchIslandByUsername(username) {
}
}
+/**
+ * Fetch posts for an island (by username)
+ */
+export async function fetchIslandPosts(usernameOrId, params = {}) {
+ const username = typeof usernameOrId === 'string' ? usernameOrId : String(usernameOrId);
+ if (!username) return [];
+ return fetchPosts({
+ username,
+ limit: params.limit || 50,
+ ...params
+ });
+}
+
export async function uploadProfileAvatar(file, opts = {}) {
if (!file) return { ok: false };
const url = `${apiBase()}/profile/avatar`;
diff --git a/src/components/World/IslandsWorld.css b/src/components/World/IslandsWorld.css
index 10b476c..760e297 100644
--- a/src/components/World/IslandsWorld.css
+++ b/src/components/World/IslandsWorld.css
@@ -16,10 +16,10 @@
height: 100%;
}
-/* Search bar - matches map overlay position */
+/* Search bar - fixed below topbar */
.islands-world-search {
- position: absolute;
- top: 0.7rem;
+ position: fixed;
+ top: 56px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
@@ -366,6 +366,180 @@
}
}
+/* Island Terrain View */
+.island-terrain-view {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ background: linear-gradient(180deg, #0a1628 0%, #0d2847 100%);
+ animation: terrainFadeIn 0.4s ease;
+}
+
+@keyframes terrainFadeIn {
+ from {
+ opacity: 0;
+ transform: scale(1.1);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.island-terrain-header {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 24px;
+ background: linear-gradient(180deg, rgba(10, 22, 40, 0.95) 0%, rgba(10, 22, 40, 0) 100%);
+}
+
+.island-terrain-back {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(56, 189, 248, 0.3);
+ border-radius: 12px;
+ color: white;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.island-terrain-back:hover {
+ background: rgba(56, 189, 248, 0.2);
+ border-color: rgba(56, 189, 248, 0.5);
+}
+
+.island-terrain-info {
+ text-align: center;
+}
+
+.island-terrain-info h2 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 700;
+ color: #38bdf8;
+}
+
+.island-terrain-type {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.6);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.island-terrain-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.island-terrain-action {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 12px;
+ color: white;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.island-terrain-action:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.island-terrain-post {
+ background: linear-gradient(135deg, #10b981, #059669);
+ border: none;
+ font-weight: 600;
+}
+
+.island-terrain-post:hover {
+ transform: scale(1.05);
+ box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
+}
+
+.island-terrain-canvas {
+ position: absolute;
+ inset: 0;
+}
+
+.island-terrain-map {
+ width: 100%;
+ height: 100%;
+}
+
+.island-terrain-loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+}
+
+.island-terrain-loading p {
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 16px;
+}
+
+/* Terrain post markers */
+.terrain-post-marker {
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.terrain-post-marker:hover {
+ transform: scale(1.1);
+ z-index: 100;
+}
+
+.terrain-post-icon {
+ width: 40px;
+ height: 40px;
+ background: linear-gradient(135deg, #38bdf8, #06b6d4);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 16px;
+ box-shadow: 0 4px 16px rgba(56, 189, 248, 0.5);
+}
+
+.terrain-post-title {
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-top: 4px;
+ padding: 4px 8px;
+ background: rgba(15, 23, 42, 0.9);
+ border-radius: 8px;
+ font-size: 11px;
+ color: white;
+ white-space: nowrap;
+ opacity: 0;
+ transition: opacity 0.2s;
+}
+
+.terrain-post-marker:hover .terrain-post-title {
+ opacity: 1;
+}
+
/* Responsive */
@media (max-width: 768px) {
.islands-world-title {
diff --git a/src/components/World/IslandsWorld.jsx b/src/components/World/IslandsWorld.jsx
index 58753fe..91e9475 100644
--- a/src/components/World/IslandsWorld.jsx
+++ b/src/components/World/IslandsWorld.jsx
@@ -2,7 +2,7 @@ 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 { fetchIslands, fetchIslandPosts } from '../../api/client';
import { useAuth } from '../../auth/AuthContext';
import './IslandsWorld.css';
@@ -14,7 +14,9 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
const { t } = useTranslation();
const { authenticated, username } = useAuth();
const containerRef = useRef(null);
+ const terrainRef = useRef(null);
const mapRef = useRef(null);
+ const terrainMapRef = useRef(null);
const markersRef = useRef([]);
const animationRef = useRef(null);
@@ -23,6 +25,11 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
const [searchQuery, setSearchQuery] = useState('');
const [hoveredIsland, setHoveredIsland] = useState(null);
+ // Terrain view state
+ const [zoomedIsland, setZoomedIsland] = useState(null);
+ const [islandPosts, setIslandPosts] = useState([]);
+ const [terrainLoading, setTerrainLoading] = useState(false);
+
// Fetch islands
useEffect(() => {
let mounted = true;
@@ -51,6 +58,35 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
)
: islands;
+ // Load island posts when zooming into terrain view
+ useEffect(() => {
+ if (!zoomedIsland) {
+ setIslandPosts([]);
+ return;
+ }
+ setTerrainLoading(true);
+ fetchIslandPosts(zoomedIsland.username || zoomedIsland.id, { limit: 50 })
+ .then(posts => {
+ setIslandPosts(posts || []);
+ setTerrainLoading(false);
+ })
+ .catch(err => {
+ console.warn('[IslandsWorld] Failed to fetch island posts:', err);
+ setIslandPosts([]);
+ setTerrainLoading(false);
+ });
+ }, [zoomedIsland]);
+
+ // Handle zoom into island terrain
+ const handleZoomToIsland = useCallback((island) => {
+ setZoomedIsland(island);
+ }, []);
+
+ // Handle back from terrain view
+ const handleBackFromTerrain = useCallback(() => {
+ setZoomedIsland(null);
+ }, []);
+
// Initialize MapLibre
useEffect(() => {
if (!containerRef.current || loading) return;
@@ -155,8 +191,8 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
window.location.href = `/island/${island.username}/edit`;
return;
}
- // Default: open island viewer
- onIslandClick?.(island);
+ // Default: zoom into island terrain
+ handleZoomToIsland(island);
});
const marker = new maplibregl.Marker({
@@ -188,7 +224,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
markersRef.current = [];
map.remove();
};
- }, [filteredIslands, loading, onIslandClick, t]);
+ }, [filteredIslands, loading, handleZoomToIsland, t, username]);
return (
@@ -241,13 +277,6 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
)}
- {/* Create Island CTA (for authenticated users without island) */}
- {authenticated && (
-
- )}
{/* Loading overlay */}
{loading && (
@@ -256,10 +285,250 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
{t('worlds.loadingIslands')}
)}
+
+ {/* Island Terrain View - Full screen when zoomed */}
+ {zoomedIsland && (
+
+
+
+
+
@{zoomedIsland.username}'s Island
+ {zoomedIsland.terrain_type || 'tropical'}
+
+
+
+ {username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
+
+ )}
+
+
+
+ {/* Terrain canvas with posts */}
+
+ {terrainLoading ? (
+
+
+
{t('worlds.loadingTerrain')}
+
+ ) : (
+
+ )}
+
+
+ )}
);
}
+/**
+ * Island Terrain Canvas - renders the island as a large terrain with posts
+ */
+function IslandTerrainCanvas({ island, posts, isOwner }) {
+ const canvasRef = useRef(null);
+ const mapRef = useRef(null);
+
+ const terrainColors = {
+ 'tropical': { main: '#2ecc71', sand: '#F5DEB3', water: '#1e90ff' },
+ 'desert': { main: '#f39c12', sand: '#EDC9AF', water: '#4aa3df' },
+ 'arctic': { main: '#3498db', sand: '#E8E8E8', water: '#5fa8d3' },
+ 'volcanic': { main: '#e74c3c', sand: '#2C2C2C', water: '#ff6b6b' },
+ 'meadow': { main: '#27ae60', sand: '#C2B280', water: '#5dade2' },
+ };
+
+ const colors = terrainColors[island.terrain_type] || terrainColors['tropical'];
+
+ useEffect(() => {
+ if (!canvasRef.current) return;
+
+ const map = new maplibregl.Map({
+ container: canvasRef.current,
+ style: {
+ version: 8,
+ sources: {},
+ layers: [
+ {
+ id: 'background',
+ type: 'background',
+ paint: { 'background-color': colors.water }
+ }
+ ]
+ },
+ center: [0, 0],
+ zoom: 3,
+ pitch: 45,
+ bearing: 0,
+ attributionControl: false
+ });
+
+ mapRef.current = map;
+
+ map.on('load', () => {
+ // Create island terrain shape
+ const seed = island.seed || (island.username || '').charCodeAt(0) || 42;
+ const terrainData = generateTerrainGeoJSON(seed, colors.main, colors.sand);
+
+ map.addSource('terrain', { type: 'geojson', data: terrainData.island });
+ map.addSource('beach', { type: 'geojson', data: terrainData.beach });
+
+ // Beach layer
+ map.addLayer({
+ id: 'beach',
+ type: 'fill',
+ source: 'beach',
+ paint: {
+ 'fill-color': colors.sand,
+ 'fill-opacity': 0.9
+ }
+ });
+
+ // Island terrain layer
+ map.addLayer({
+ id: 'terrain',
+ type: 'fill',
+ source: 'terrain',
+ paint: {
+ 'fill-color': colors.main,
+ 'fill-opacity': 1
+ }
+ });
+
+ // Add terrain details (trees, rocks, etc.)
+ const detailsData = generateTerrainDetails(seed);
+ map.addSource('details', { type: 'geojson', data: detailsData });
+ map.addLayer({
+ id: 'details',
+ type: 'circle',
+ source: 'details',
+ paint: {
+ 'circle-radius': ['get', 'size'],
+ 'circle-color': ['get', 'color'],
+ 'circle-opacity': 0.8
+ }
+ });
+
+ // Add posts as markers on the terrain
+ posts.forEach((post, idx) => {
+ const angle = (idx / Math.max(posts.length, 1)) * Math.PI * 2;
+ const radius = 0.01 + Math.random() * 0.02;
+ const lng = Math.cos(angle) * radius;
+ const lat = Math.sin(angle) * radius;
+
+ const el = document.createElement('div');
+ el.className = 'terrain-post-marker';
+ el.innerHTML = `
+
+
+
+ ${(post.title || post.caption || '').slice(0, 30)}...
+ `;
+ el.addEventListener('click', () => {
+ window.dispatchEvent(new CustomEvent('sw:openPostDetail', { detail: { post } }));
+ });
+
+ new maplibregl.Marker({ element: el, anchor: 'bottom' })
+ .setLngLat([lng, lat])
+ .addTo(map);
+ });
+ });
+
+ return () => {
+ map.remove();
+ };
+ }, [island, posts, colors]);
+
+ return ;
+}
+
+/**
+ * Generate terrain GeoJSON for island view
+ */
+function generateTerrainGeoJSON(seed, mainColor, sandColor) {
+ const rng = seededRandom(seed);
+ const numPoints = 24;
+ const islandPoints = [];
+ const beachPoints = [];
+
+ for (let i = 0; i < numPoints; i++) {
+ const angle = (i / numPoints) * Math.PI * 2;
+ const islandRadius = 0.025 + rng() * 0.015;
+ const beachRadius = islandRadius * 1.15;
+
+ islandPoints.push([Math.cos(angle) * islandRadius, Math.sin(angle) * islandRadius]);
+ beachPoints.push([Math.cos(angle) * beachRadius, Math.sin(angle) * beachRadius]);
+ }
+
+ // Close the polygons
+ islandPoints.push(islandPoints[0]);
+ beachPoints.push(beachPoints[0]);
+
+ return {
+ island: {
+ type: 'Feature',
+ geometry: { type: 'Polygon', coordinates: [islandPoints] }
+ },
+ beach: {
+ type: 'Feature',
+ geometry: { type: 'Polygon', coordinates: [beachPoints] }
+ }
+ };
+}
+
+/**
+ * Generate terrain details (trees, rocks, flowers)
+ */
+function generateTerrainDetails(seed) {
+ const rng = seededRandom(seed);
+ const features = [];
+ const detailCount = 30 + Math.floor(rng() * 20);
+
+ for (let i = 0; i < detailCount; i++) {
+ const angle = rng() * Math.PI * 2;
+ const radius = rng() * 0.02;
+ const lng = Math.cos(angle) * radius;
+ const lat = Math.sin(angle) * radius;
+
+ const type = rng();
+ let color, size;
+ if (type < 0.5) {
+ // Tree
+ color = '#228B22';
+ size = 4 + rng() * 3;
+ } else if (type < 0.75) {
+ // Rock
+ color = '#696969';
+ size = 2 + rng() * 2;
+ } else {
+ // Flower
+ color = ['#FF69B4', '#FFD700', '#FF6347', '#9370DB'][Math.floor(rng() * 4)];
+ size = 2 + rng();
+ }
+
+ features.push({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [lng, lat] },
+ properties: { color, size }
+ });
+ }
+
+ return { type: 'FeatureCollection', features };
+}
+
/**
* Create a stylized island marker element
*/
diff --git a/src/components/World/PlanetsWorld.css b/src/components/World/PlanetsWorld.css
index 5524a39..29ebec0 100644
--- a/src/components/World/PlanetsWorld.css
+++ b/src/components/World/PlanetsWorld.css
@@ -16,10 +16,10 @@
height: 100%;
}
-/* Search bar - matches map overlay position */
+/* Search bar - fixed below topbar */
.planets-world-search {
- position: absolute;
- top: 0.7rem;
+ position: fixed;
+ top: 56px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
@@ -399,36 +399,147 @@
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.5);
}
-/* Create Planet button */
-.planets-world-create {
+/* Planet Terrain View */
+.planet-terrain-view {
position: fixed;
- bottom: 24px;
- right: 24px;
- z-index: 200;
+ inset: 0;
+ z-index: 500;
+ background: linear-gradient(180deg, #0f0f1a 0%, #050510 100%);
+ animation: planetTerrainFadeIn 0.4s ease;
+}
+
+@keyframes planetTerrainFadeIn {
+ from {
+ opacity: 0;
+ transform: scale(1.1);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.planet-terrain-header {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
display: flex;
align-items: center;
- gap: 10px;
- background: linear-gradient(135deg, #9b59b6, #8e44ad);
- border: none;
- border-radius: 50px;
- padding: 14px 24px;
+ justify-content: space-between;
+ padding: 16px 24px;
+ background: linear-gradient(180deg, rgba(15, 15, 26, 0.95) 0%, rgba(15, 15, 26, 0) 100%);
+}
+
+.planet-terrain-back {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(155, 89, 182, 0.3);
+ border-radius: 12px;
color: white;
font-size: 14px;
- font-weight: 700;
+ font-weight: 600;
cursor: pointer;
- box-shadow: 0 8px 32px rgba(155, 89, 182, 0.4);
- transition: all 0.3s ease;
+ transition: all 0.2s ease;
}
-.planets-world-create:hover {
+.planet-terrain-back:hover {
+ background: rgba(155, 89, 182, 0.2);
+ border-color: rgba(155, 89, 182, 0.5);
+}
+
+.planet-terrain-info {
+ text-align: center;
+}
+
+.planet-terrain-info h2 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 700;
+ color: #d4a5ff;
+}
+
+.planet-terrain-theme {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.6);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.planet-terrain-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.planet-terrain-action {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 12px;
+ color: white;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.planet-terrain-action:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.planet-terrain-post {
+ background: linear-gradient(135deg, #9b59b6, #8e44ad);
+ border: none;
+ font-weight: 600;
+}
+
+.planet-terrain-post:hover {
transform: scale(1.05);
- box-shadow: 0 12px 40px rgba(155, 89, 182, 0.5);
+ box-shadow: 0 4px 16px rgba(155, 89, 182, 0.4);
}
-.planets-world-create i {
+.planet-terrain-canvas {
+ position: absolute;
+ inset: 0;
+}
+
+.planet-terrain-map {
+ width: 100%;
+ height: 100%;
+}
+
+.planet-terrain-loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+}
+
+.planet-terrain-loading p {
+ color: rgba(255, 255, 255, 0.8);
font-size: 16px;
}
+/* Planet post markers on terrain */
+.planet-post-marker {
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.planet-post-marker:hover {
+ transform: scale(1.1);
+ z-index: 100;
+}
+
/* Loading overlay */
.planets-world-loading {
position: absolute;
diff --git a/src/components/World/PlanetsWorld.jsx b/src/components/World/PlanetsWorld.jsx
index b05b232..d5a326f 100644
--- a/src/components/World/PlanetsWorld.jsx
+++ b/src/components/World/PlanetsWorld.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+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';
@@ -12,7 +12,7 @@ import './PlanetsWorld.css';
*/
export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
const { t } = useTranslation();
- const { authenticated } = useAuth();
+ const { authenticated, username } = useAuth();
const containerRef = useRef(null);
const mapRef = useRef(null);
const markersRef = useRef([]);
@@ -22,7 +22,11 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [hoveredPlanet, setHoveredPlanet] = useState(null);
- const [selectedPlanet, setSelectedPlanet] = useState(null);
+
+ // Terrain view state
+ const [zoomedPlanet, setZoomedPlanet] = useState(null);
+ const [planetPosts, setPlanetPosts] = useState([]);
+ const [terrainLoading, setTerrainLoading] = useState(false);
// Fetch planets
useEffect(() => {
@@ -52,6 +56,31 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
)
: planets;
+ // Load planet posts when zooming into terrain view
+ useEffect(() => {
+ if (!zoomedPlanet) {
+ setPlanetPosts([]);
+ return;
+ }
+ setTerrainLoading(true);
+ // TODO: Add fetchPlanetPosts API function
+ // For now, simulate with empty posts
+ setTimeout(() => {
+ setPlanetPosts([]);
+ setTerrainLoading(false);
+ }, 500);
+ }, [zoomedPlanet]);
+
+ // Handle zoom into planet terrain
+ const handleZoomToPlanet = useCallback((planet) => {
+ setZoomedPlanet(planet);
+ }, []);
+
+ // Handle back from terrain view
+ const handleBackFromTerrain = useCallback(() => {
+ setZoomedPlanet(null);
+ }, []);
+
// Initialize MapLibre
useEffect(() => {
if (!containerRef.current || loading) return;
@@ -115,7 +144,6 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
// 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);
@@ -132,7 +160,7 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
el.addEventListener('mouseleave', () => setHoveredPlanet(null));
el.addEventListener('click', (e) => {
e.stopPropagation();
- setSelectedPlanet(planet);
+ handleZoomToPlanet(planet);
});
const marker = new maplibregl.Marker({
@@ -164,7 +192,7 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
markersRef.current = [];
map.remove();
};
- }, [filteredPlanets, loading, t]);
+ }, [filteredPlanets, loading, t, handleZoomToPlanet]);
return (
@@ -198,7 +226,7 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
{/* Hovered planet tooltip */}
- {hoveredPlanet && (
+ {hoveredPlanet && !zoomedPlanet && (
@@ -213,59 +241,6 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
)}
- {/* 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 && (
@@ -273,10 +248,232 @@ export default function PlanetsWorld({ theme, onOpenMap, onOpenIslands }) {
{t('worlds.loadingPlanets')}
)}
+
+ {/* Planet Terrain View - Full screen when zoomed */}
+ {zoomedPlanet && (
+
+
+
+
+
{zoomedPlanet.name || 'Planet'}
+ {zoomedPlanet.theme || 'default'}
+
+
+
+ {authenticated && (
+
+ )}
+
+
+
+ {/* Terrain canvas with posts */}
+
+ {terrainLoading ? (
+
+
+
{t('worlds.loadingTerrain')}
+
+ ) : (
+
+ )}
+
+
+ )}
);
}
+/**
+ * Planet Terrain Canvas - renders the planet as a large terrain with posts
+ */
+function PlanetTerrainCanvas({ planet, posts }) {
+ const canvasRef = useRef(null);
+ const mapRef = useRef(null);
+
+ const themeColors = {
+ 'default': { main: '#9b59b6', accent: '#8e44ad', bg: '#1a0a2e' },
+ 'tech': { main: '#3498db', accent: '#2980b9', bg: '#0a1a2e' },
+ 'gaming': { main: '#e74c3c', accent: '#c0392b', bg: '#2e0a0a' },
+ 'music': { main: '#1abc9c', accent: '#16a085', bg: '#0a2e2a' },
+ 'art': { main: '#e91e63', accent: '#c2185b', bg: '#2e0a1a' },
+ 'science': { main: '#00bcd4', accent: '#0097a7', bg: '#0a2e2e' },
+ 'sports': { main: '#ff9800', accent: '#f57c00', bg: '#2e1a0a' },
+ };
+
+ const colors = themeColors[planet.theme] || themeColors['default'];
+
+ useEffect(() => {
+ if (!canvasRef.current) return;
+
+ const map = new maplibregl.Map({
+ container: canvasRef.current,
+ style: {
+ version: 8,
+ sources: {},
+ layers: [
+ {
+ id: 'background',
+ type: 'background',
+ paint: { 'background-color': colors.bg }
+ }
+ ]
+ },
+ center: [0, 0],
+ zoom: 3,
+ pitch: 45,
+ bearing: 0,
+ attributionControl: false
+ });
+
+ mapRef.current = map;
+
+ map.on('load', () => {
+ // Create planet surface with craters/features
+ const seed = planet.id || (planet.name || '').charCodeAt(0) || 42;
+ const surfaceData = generatePlanetSurface(seed, colors.main, colors.accent);
+
+ map.addSource('surface', { type: 'geojson', data: surfaceData.terrain });
+ map.addSource('features', { type: 'geojson', data: surfaceData.features });
+
+ // Planet surface
+ map.addLayer({
+ id: 'surface',
+ type: 'fill',
+ source: 'surface',
+ paint: {
+ 'fill-color': colors.main,
+ 'fill-opacity': 1
+ }
+ });
+
+ // Surface features (craters, mountains)
+ map.addLayer({
+ id: 'features',
+ type: 'circle',
+ source: 'features',
+ paint: {
+ 'circle-radius': ['get', 'size'],
+ 'circle-color': ['get', 'color'],
+ 'circle-opacity': ['get', 'opacity']
+ }
+ });
+
+ // Add posts as markers on the terrain
+ posts.forEach((post, idx) => {
+ const angle = (idx / Math.max(posts.length, 1)) * Math.PI * 2;
+ const radius = 0.01 + Math.random() * 0.02;
+ const lng = Math.cos(angle) * radius;
+ const lat = Math.sin(angle) * radius;
+
+ const el = document.createElement('div');
+ el.className = 'terrain-post-marker planet-post-marker';
+ el.innerHTML = `
+
+
+
+
${(post.title || post.caption || '').slice(0, 30)}...
+ `;
+ el.addEventListener('click', () => {
+ window.dispatchEvent(new CustomEvent('sw:openPostDetail', { detail: { post } }));
+ });
+
+ new maplibregl.Marker({ element: el, anchor: 'bottom' })
+ .setLngLat([lng, lat])
+ .addTo(map);
+ });
+ });
+
+ return () => {
+ map.remove();
+ };
+ }, [planet, posts, colors]);
+
+ return
;
+}
+
+/**
+ * Generate planet surface GeoJSON
+ */
+function generatePlanetSurface(seed, mainColor, accentColor) {
+ const rng = seededRandom(seed);
+ const numPoints = 32;
+ const surfacePoints = [];
+
+ for (let i = 0; i < numPoints; i++) {
+ const angle = (i / numPoints) * Math.PI * 2;
+ const radius = 0.03 + rng() * 0.01;
+ surfacePoints.push([Math.cos(angle) * radius, Math.sin(angle) * radius]);
+ }
+ surfacePoints.push(surfacePoints[0]);
+
+ // Generate craters and features
+ const features = [];
+ const featureCount = 20 + Math.floor(rng() * 15);
+
+ for (let i = 0; i < featureCount; i++) {
+ const angle = rng() * Math.PI * 2;
+ const radius = rng() * 0.025;
+ const lng = Math.cos(angle) * radius;
+ const lat = Math.sin(angle) * radius;
+
+ const type = rng();
+ let color, size, opacity;
+ if (type < 0.4) {
+ // Crater
+ color = accentColor;
+ size = 3 + rng() * 4;
+ opacity = 0.6;
+ } else if (type < 0.7) {
+ // Mountain/ridge
+ color = mainColor;
+ size = 2 + rng() * 3;
+ opacity = 0.8;
+ } else {
+ // Glowing spot
+ color = '#ffffff';
+ size = 1 + rng() * 2;
+ opacity = 0.4;
+ }
+
+ features.push({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [lng, lat] },
+ properties: { color, size, opacity }
+ });
+ }
+
+ return {
+ terrain: {
+ type: 'Feature',
+ geometry: { type: 'Polygon', coordinates: [surfacePoints] }
+ },
+ features: { type: 'FeatureCollection', features }
+ };
+}
+
+/**
+ * Seeded random number generator
+ */
+function seededRandom(seed) {
+ let s = seed;
+ return function() {
+ s = (s * 9301 + 49297) % 233280;
+ return s / 233280;
+ };
+}
+
/**
* Create a stylized planet marker element
*/