From 6e29a502d481e3d1234eff5d88be70a12f55f953 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 19 Jan 2026 09:37:57 +0000 Subject: [PATCH] feat: Islands overhaul + modal improvements Islands Universe: - Grid-based positioning to prevent overlap - Much bigger islands (120px) with detailed SVG - Beach rings, palm trees, huts, sparkles - Info cards with username, post count, terrain type - Zoomed out view to see all islands Island Viewer: - Bigger island visualization (320-400px) - Terrain-specific decorations (emoji) - Add Post button for island owners - Share Island button with native share API Post Modal: - Hide "Open source" button when no URL - Changed to link instead of disabled button Co-Authored-By: Claude Opus 4.5 --- src/components/Island/IslandUniverse.jsx | 173 +++++++++++++++++------ src/components/Island/IslandViewer.jsx | 101 +++++++++++-- src/components/Map/markerManager.js | 10 +- src/styles/island.css | 68 +++++++++ 4 files changed, 292 insertions(+), 60 deletions(-) diff --git a/src/components/Island/IslandUniverse.jsx b/src/components/Island/IslandUniverse.jsx index 44bdd90..cee77cf 100644 --- a/src/components/Island/IslandUniverse.jsx +++ b/src/components/Island/IslandUniverse.jsx @@ -51,8 +51,8 @@ export default function IslandUniverse({ onClose, onIslandClick }) { ] }, center: [centerLng, centerLat], - zoom: 0.8, - pitch: 70, // 3D angle + zoom: 0.5, // Zoomed out more to see spread islands + pitch: 65, // 3D angle bearing: 0, antialias: true, attributionControl: false @@ -64,14 +64,21 @@ export default function IslandUniverse({ onClose, onIslandClick }) { // Create island markers const markers = []; + // Use grid-based positioning to prevent overlap + const gridSize = Math.ceil(Math.sqrt(islands.length)); + const spacing = 8; // degrees between islands + islands.forEach((island, idx) => { - // Truly scatter islands randomly across wide area - const randomAngle = Math.random() * Math.PI * 2; - const randomRadius = 5 + Math.random() * 15; // Large random radius 5-20 degrees - const randomOffsetX = (Math.random() - 0.5) * 10; // Additional random scatter - const randomOffsetY = (Math.random() - 0.5) * 10; - const lng = centerLng + Math.cos(randomAngle) * randomRadius + randomOffsetX; - const lat = centerLat + Math.sin(randomAngle) * randomRadius + randomOffsetY; + // Grid position with random offset for organic feel + const gridX = idx % gridSize; + 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; + const lng = centerLng + baseX + jitterX; + const lat = centerLat + baseY + jitterY; // Create marker element const el = document.createElement('div'); @@ -81,32 +88,46 @@ export default function IslandUniverse({ onClose, onIslandClick }) { const islandSVG = generateMiniIsland(island.seed || idx, terrainColor); + const postCount = island.content_count || 0; + const terrain = island.terrain_type || 'tropical'; + el.innerHTML = `
${islandSVG} +
@${island.username}
+ box-shadow: 0 4px 16px rgba(0,0,0,0.5), 0 0 12px ${terrainColor}33; + backdrop-filter: blur(8px); + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + "> + @${island.username} + + ${postCount} posts • ${terrain} + +
`; @@ -260,7 +281,7 @@ function getTerrainColor(terrainType) { } /** - * Generate a mini island SVG shape (organic, irregular) + * Generate a detailed island SVG shape (bigger, more features) */ function generateMiniIsland(seed, terrainColor) { // Seeded random @@ -269,57 +290,127 @@ function generateMiniIsland(seed, terrainColor) { return seed / 233280; }; - // Generate organic island shape using bezier curves - const size = 50; + // Much bigger island + const size = 120; const centerX = size / 2; const centerY = size / 2; - const numPoints = 8; + const numPoints = 12; let path = ''; const points = []; for (let i = 0; i < numPoints; i++) { const angle = (i / numPoints) * Math.PI * 2; - const radiusVariation = 0.7 + rng() * 0.6; // Random radius - const radius = (size / 2) * radiusVariation; + const radiusVariation = 0.65 + rng() * 0.5; + const radius = (size / 2.2) * radiusVariation; const x = centerX + Math.cos(angle) * radius; const y = centerY + Math.sin(angle) * radius; points.push({ x, y }); } - // Create smooth curve using quadratic bezier path = `M ${points[0].x} ${points[0].y}`; for (let i = 0; i < numPoints; i++) { const current = points[i]; const next = points[(i + 1) % numPoints]; - const controlX = (current.x + next.x) / 2 + (rng() - 0.5) * 5; - const controlY = (current.y + next.y) / 2 + (rng() - 0.5) * 5; + const controlX = (current.x + next.x) / 2 + (rng() - 0.5) * 8; + const controlY = (current.y + next.y) / 2 + (rng() - 0.5) * 8; path += ` Q ${controlX} ${controlY} ${next.x} ${next.y}`; } path += ' Z'; - // Add some trees/details - const numTrees = 2 + Math.floor(rng() * 3); + // Beach ring + let beachPath = ''; + const beachPoints = []; + for (let i = 0; i < numPoints; i++) { + const angle = (i / numPoints) * Math.PI * 2; + const radiusVariation = 0.72 + rng() * 0.45; + const radius = (size / 2.1) * radiusVariation; + beachPoints.push({ + x: centerX + Math.cos(angle) * radius, + y: centerY + Math.sin(angle) * radius + }); + } + beachPath = `M ${beachPoints[0].x} ${beachPoints[0].y}`; + for (let i = 0; i < numPoints; i++) { + const current = beachPoints[i]; + const next = beachPoints[(i + 1) % numPoints]; + beachPath += ` Q ${(current.x + next.x) / 2} ${(current.y + next.y) / 2} ${next.x} ${next.y}`; + } + beachPath += ' Z'; + + // Trees (more, varied sizes) + const numTrees = 5 + Math.floor(rng() * 6); let trees = ''; for (let i = 0; i < numTrees; i++) { - const tx = centerX + (rng() - 0.5) * (size * 0.4); - const ty = centerY + (rng() - 0.5) * (size * 0.4); - const treeSize = 3 + rng() * 2; - trees += ``; + const angle = rng() * Math.PI * 2; + const dist = 8 + rng() * 25; + const tx = centerX + Math.cos(angle) * dist; + const ty = centerY + Math.sin(angle) * dist; + const treeHeight = 8 + rng() * 10; + const treeWidth = 4 + rng() * 4; + // Palm tree shape + trees += ` + + + + + + `; + } + + // Small buildings/huts + const numHuts = 1 + Math.floor(rng() * 2); + let huts = ''; + for (let i = 0; i < numHuts; i++) { + const angle = rng() * Math.PI * 2; + const dist = 10 + rng() * 15; + const hx = centerX + Math.cos(angle) * dist; + const hy = centerY + Math.sin(angle) * dist; + huts += ` + + + + + `; + } + + // Water sparkles + let sparkles = ''; + for (let i = 0; i < 6; i++) { + const sx = rng() * size; + const sy = rng() * size; + sparkles += ``; } return ` - + - - + + + + + + + + + + + + + - - - + + ${sparkles} + + + + + + ${huts} + ${trees} `; diff --git a/src/components/Island/IslandViewer.jsx b/src/components/Island/IslandViewer.jsx index a0661a8..76b7f1a 100644 --- a/src/components/Island/IslandViewer.jsx +++ b/src/components/Island/IslandViewer.jsx @@ -491,6 +491,42 @@ export default function IslandViewer({ island, onClose }) { + {/* Action buttons */} +
+ {isSelf && ( + + )} + +
+

🌱 Seed: {island.seed} • 🎨 {island.color_palette || 'vibrant'} palette @@ -605,37 +641,74 @@ function createIslandElement(seed, terrainType = 'tropical') { el.className = 'island-terrain-marker'; const terrainColor = getTerrainColorCSS(terrainType); - const size = 220 + rng() * 50; // 220-270px + const size = 320 + rng() * 80; // Much bigger: 320-400px + + // Terrain-specific decorations + const terrainEmoji = { + 'tropical': '🌴', + 'desert': '🌵', + 'arctic': '❄️', + 'volcanic': '🌋', + 'meadow': '🌸' + }[terrainType] || '🌴'; el.innerHTML = `

+
+
+ +
+ ">${terrainEmoji}
+
${terrainEmoji}
+
🏠
${createIslandTexture(seed, terrainType)}
`; diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js index 7c0707c..df5f4f1 100644 --- a/src/components/Map/markerManager.js +++ b/src/components/Map/markerManager.js @@ -1476,11 +1476,11 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) { Watch Live Stream - ` : ` - - `} + ` : url ? ` + + Open source + + ` : ``}
${!isCamera ? `` : ''} diff --git a/src/styles/island.css b/src/styles/island.css index 3ab5972..ffb6ed3 100644 --- a/src/styles/island.css +++ b/src/styles/island.css @@ -688,3 +688,71 @@ body[data-theme="light"] .password-modal-field input { color: #1e293b; border-color: rgba(99, 102, 241, 0.3); } + +/* ===== ISLAND ACTION BUTTONS ===== */ + +.island-viewer-actions { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 1.5rem; + flex-wrap: wrap; +} + +.island-action-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + border-radius: 12px; + font-size: 0.9rem; + font-weight: 700; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid rgba(148, 163, 184, 0.3); + background: rgba(30, 41, 59, 0.6); + color: #e2e8f0; + backdrop-filter: blur(8px); +} + +.island-action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + border-color: rgba(96, 165, 250, 0.5); +} + +.island-action-btn:active { + transform: scale(0.97); +} + +.island-action-btn i { + font-size: 1rem; +} + +.island-action-primary { + background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%); + border-color: rgba(99, 102, 241, 0.5); + color: white; + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3); +} + +.island-action-primary:hover { + background: linear-gradient(135deg, #60a5fa 0%, #818cf8 100%); + box-shadow: 0 8px 28px rgba(99, 102, 241, 0.4); + border-color: rgba(129, 140, 248, 0.6); +} + +body[data-theme="light"] .island-action-btn { + background: rgba(255, 255, 255, 0.8); + color: #1e3a8a; + border-color: rgba(99, 102, 241, 0.25); +} + +body[data-theme="light"] .island-action-btn:hover { + border-color: rgba(99, 102, 241, 0.5); +} + +body[data-theme="light"] .island-action-primary { + background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); + color: white; +}