From 6422aeebfe70169c2eaa5166ae51e1dd0b32e9d3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 24 Jan 2026 22:52:05 +0000 Subject: [PATCH] v112: Scatter islands randomly + full-screen terrain zoom with posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Islands now scattered across 300° world area using seeded random positions - When clicking island, zooms to fill screen (zoom 16, pitch 0) - Full-screen terrain overlay shows posts scattered on the island - Posts positioned pseudo-randomly based on island seed - Loading state while fetching island posts - Empty state with "create first post" CTA for island owner Co-Authored-By: Claude Opus 4.5 --- package.json | 2 +- src/components/World/IslandsWorld.css | 192 +++++++++++++++++++++++--- src/components/World/IslandsWorld.jsx | 155 ++++++++++++++++----- 3 files changed, 292 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index e01cdc0..a58134c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sociowire-frontend", "private": true, - "version": "0.0.111", + "version": "0.0.112", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/src/components/World/IslandsWorld.css b/src/components/World/IslandsWorld.css index 683a77a..7c77e58 100644 --- a/src/components/World/IslandsWorld.css +++ b/src/components/World/IslandsWorld.css @@ -366,26 +366,38 @@ } } -/* Island Zoomed Overlay */ -.island-zoomed-overlay { +/* Island Terrain Overlay - Full screen when zoomed */ +.island-terrain-overlay { position: fixed; + inset: 0; + z-index: 500; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.island-terrain-overlay.zooming { + opacity: 0.5; +} + +.island-terrain-overlay.ready { + opacity: 1; +} + +.island-terrain-header { + position: absolute; top: 0; left: 0; right: 0; - z-index: 500; - pointer-events: none; -} - -.island-zoomed-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; background: linear-gradient(180deg, rgba(10, 22, 40, 0.95) 0%, rgba(10, 22, 40, 0) 100%); pointer-events: auto; + z-index: 10; } -.island-zoomed-back { +.island-terrain-back { display: flex; align-items: center; gap: 8px; @@ -398,41 +410,41 @@ font-weight: 600; cursor: pointer; transition: all 0.2s ease; + backdrop-filter: blur(8px); } -.island-zoomed-back:hover { +.island-terrain-back:hover { background: rgba(56, 189, 248, 0.2); border-color: rgba(56, 189, 248, 0.5); } -.island-zoomed-info { +.island-terrain-title { text-align: center; } -.island-zoomed-info h2 { +.island-terrain-title h2 { margin: 0; font-size: 20px; font-weight: 700; color: #38bdf8; } -.island-zoomed-info span { +.island-terrain-title span { font-size: 12px; color: rgba(255, 255, 255, 0.6); - text-transform: uppercase; - letter-spacing: 1px; } -.island-zoomed-actions { +.island-terrain-actions { display: flex; gap: 10px; } -.island-zoomed-action { +.island-terrain-btn { display: flex; align-items: center; + justify-content: center; gap: 8px; - padding: 10px 16px; + padding: 10px 14px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; @@ -440,23 +452,161 @@ font-size: 14px; cursor: pointer; transition: all 0.2s ease; + backdrop-filter: blur(8px); } -.island-zoomed-action:hover { - background: rgba(255, 255, 255, 0.15); +.island-terrain-btn:hover { + background: rgba(255, 255, 255, 0.2); } -.island-zoomed-post { +.island-terrain-post-btn { background: linear-gradient(135deg, #10b981, #059669); border: none; font-weight: 600; } -.island-zoomed-post:hover { +.island-terrain-post-btn:hover { transform: scale(1.05); box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4); } +/* Posts container - overlay for posts on island */ +.island-terrain-posts { + position: absolute; + inset: 0; + pointer-events: none; +} + +.island-terrain-loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.island-terrain-loading .loading-spinner { + width: 40px; + height: 40px; +} + +.island-terrain-empty { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + color: rgba(255, 255, 255, 0.6); + pointer-events: auto; +} + +.island-terrain-empty i { + font-size: 48px; + color: rgba(56, 189, 248, 0.4); + margin-bottom: 16px; + display: block; +} + +.island-terrain-empty p { + font-size: 16px; + margin-bottom: 20px; +} + +.island-terrain-first-post { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + background: linear-gradient(135deg, #10b981, #059669); + border: none; + border-radius: 50px; + color: white; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.island-terrain-first-post:hover { + transform: scale(1.05); + box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4); +} + +/* Post markers on the island terrain */ +.island-post-marker { + position: absolute; + pointer-events: auto; + cursor: pointer; + transition: transform 0.3s ease, box-shadow 0.3s ease; + animation: postFloat 4s ease-in-out infinite; +} + +.island-post-marker:hover { + transform: scale(1.1) !important; + z-index: 100; +} + +@keyframes postFloat { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-6px); } +} + +.island-post-image { + width: 100px; + height: 100px; + border-radius: 12px; + overflow: hidden; + background: rgba(15, 23, 42, 0.9); + border: 2px solid rgba(56, 189, 248, 0.4); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); +} + +.island-post-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.island-post-text { + max-width: 150px; + padding: 12px 16px; + background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(30, 41, 59, 0.9)); + border-radius: 12px; + border: 1px solid rgba(56, 189, 248, 0.3); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(8px); +} + +.island-post-text p { + margin: 0; + font-size: 12px; + color: rgba(255, 255, 255, 0.9); + line-height: 1.4; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} + +.island-post-meta { + position: absolute; + bottom: -8px; + right: -8px; + background: rgba(15, 23, 42, 0.95); + padding: 4px 10px; + border-radius: 20px; + border: 1px solid rgba(239, 68, 68, 0.5); +} + +.island-post-likes { + font-size: 11px; + color: #ef4444; + font-weight: 600; +} + +.island-post-likes i { + margin-right: 4px; +} + /* Responsive */ @media (max-width: 768px) { .islands-world-title { diff --git a/src/components/World/IslandsWorld.jsx b/src/components/World/IslandsWorld.jsx index 1f0f089..7f27494 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, fetchIslandPosts } from '../../api/client'; +import { fetchIslands, fetchIslandPosts, fetchPosts } from '../../api/client'; import { useAuth } from '../../auth/AuthContext'; import './IslandsWorld.css'; @@ -27,6 +27,8 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl // Zoomed island state const [zoomedIsland, setZoomedIsland] = useState(null); const [isZooming, setIsZooming] = useState(false); + const [islandPosts, setIslandPosts] = useState([]); + const [loadingPosts, setLoadingPosts] = useState(false); // Fetch islands useEffect(() => { @@ -56,8 +58,8 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl ) : islands; - // Handle zoom to island - const handleZoomToIsland = useCallback((island) => { + // Handle zoom to island - fills the screen with the island terrain + const handleZoomToIsland = useCallback(async (island) => { if (!mapRef.current || isZooming) return; const pos = islandPositionsRef.current.get(island.username || island.id); @@ -65,6 +67,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl setIsZooming(true); setZoomedIsland(island); + setLoadingPosts(true); // Stop the rotation animation if (animationRef.current) { @@ -72,16 +75,26 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl animationRef.current = null; } - // Fly to island position with high zoom + // Fly to island - zoom high enough so island fills screen mapRef.current.flyTo({ center: [pos.lng, pos.lat], - zoom: 6, - pitch: 60, + zoom: 16, + pitch: 0, bearing: 0, - duration: 1500, + duration: 2000, essential: true }); + // Fetch posts for this island + try { + const posts = await fetchPosts({ username: island.username, limit: 50 }); + setIslandPosts(posts || []); + } catch (err) { + console.warn('[IslandsWorld] Failed to fetch island posts:', err); + setIslandPosts([]); + } + setLoadingPosts(false); + mapRef.current.once('moveend', () => { setIsZooming(false); }); @@ -92,6 +105,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl if (!mapRef.current || isZooming) return; setIsZooming(true); + setIslandPosts([]); mapRef.current.flyTo({ center: [0, 0], @@ -181,20 +195,16 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl } }); - // Create island markers + // Create island markers - scattered randomly across a large area const markers = []; - const gridSize = Math.ceil(Math.sqrt(filteredIslands.length || 1)); - const spacing = 20; + const worldSize = 300; // Large world area 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; + // Use seeded random for consistent positions per island + const rng = seededRandom((island.username || island.id || '').charCodeAt(0) * 1000 + idx); + // Random position across the entire world + const lng = (rng() - 0.5) * worldSize; + const lat = (rng() - 0.5) * worldSize; // Store position for zoom islandPositionsRef.current.set(island.username || island.id, { lng, lat }); @@ -301,34 +311,68 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl )} - {/* Zoomed island UI overlay */} - {zoomedIsland && !isZooming && ( -
-
- -
+

@{zoomedIsland.username}

- {zoomedIsland.terrain_type || 'tropical'} + {zoomedIsland.display_name || zoomedIsland.terrain_type || 'Island'}
-
- {username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && ( - + <> + + + )}
+ + {/* Posts on the island terrain */} +
+ {loadingPosts && ( +
+
+
+ )} + {!loadingPosts && islandPosts.length === 0 && ( +
+ +

{t('worlds.emptyIsland')}

+ {username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && ( + + )} +
+ )} + {!loadingPosts && islandPosts.map((post, idx) => ( + + ))} +
)} @@ -495,3 +539,44 @@ function seededRandom(seed) { return s / 233280; }; } + +/** + * IslandPost - A post displayed on the island terrain + */ +function IslandPost({ post, index, islandSeed }) { + // Position posts pseudo-randomly on the island + const rng = seededRandom(islandSeed * 100 + index + (post.id || 0)); + const left = 10 + rng() * 75; // 10-85% from left + const top = 15 + rng() * 65; // 15-80% from top + const rotation = (rng() - 0.5) * 12; // -6 to +6 degrees + + const hasImage = post.media_urls?.length > 0 || post.image_url; + const imageUrl = post.media_urls?.[0] || post.image_url; + + return ( +
window.location.href = `/post/${post.id}`} + > + {hasImage ? ( +
+ +
+ ) : ( +
+

{(post.content || post.text || '').slice(0, 80)}

+
+ )} +
+ + {post.like_count || 0} + +
+
+ ); +}