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 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-19 09:37:57 +00:00
parent fa345f23fd
commit 6e29a502d4
4 changed files with 292 additions and 60 deletions

View File

@ -51,8 +51,8 @@ export default function IslandUniverse({ onClose, onIslandClick }) {
] ]
}, },
center: [centerLng, centerLat], center: [centerLng, centerLat],
zoom: 0.8, zoom: 0.5, // Zoomed out more to see spread islands
pitch: 70, // 3D angle pitch: 65, // 3D angle
bearing: 0, bearing: 0,
antialias: true, antialias: true,
attributionControl: false attributionControl: false
@ -64,14 +64,21 @@ export default function IslandUniverse({ onClose, onIslandClick }) {
// Create island markers // Create island markers
const 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) => { islands.forEach((island, idx) => {
// Truly scatter islands randomly across wide area // Grid position with random offset for organic feel
const randomAngle = Math.random() * Math.PI * 2; const gridX = idx % gridSize;
const randomRadius = 5 + Math.random() * 15; // Large random radius 5-20 degrees const gridY = Math.floor(idx / gridSize);
const randomOffsetX = (Math.random() - 0.5) * 10; // Additional random scatter const baseX = (gridX - gridSize / 2) * spacing;
const randomOffsetY = (Math.random() - 0.5) * 10; const baseY = (gridY - gridSize / 2) * spacing;
const lng = centerLng + Math.cos(randomAngle) * randomRadius + randomOffsetX; // Add controlled randomness within cell
const lat = centerLat + Math.sin(randomAngle) * randomRadius + randomOffsetY; 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 // Create marker element
const el = document.createElement('div'); const el = document.createElement('div');
@ -81,32 +88,46 @@ export default function IslandUniverse({ onClose, onIslandClick }) {
const islandSVG = generateMiniIsland(island.seed || idx, terrainColor); const islandSVG = generateMiniIsland(island.seed || idx, terrainColor);
const postCount = island.content_count || 0;
const terrain = island.terrain_type || 'tropical';
el.innerHTML = ` el.innerHTML = `
<div class="universe-island-shape" style=" <div class="universe-island-shape" style="
position: relative; position: relative;
cursor: pointer; cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
animation: universeIslandFloat 6s ease-in-out infinite; animation: universeIslandFloat 6s ease-in-out infinite;
animation-delay: ${idx * 0.5}s; animation-delay: ${(idx % 10) * 0.3}s;
filter: drop-shadow(0 0 15px ${terrainColor}aa) drop-shadow(0 8px 20px rgba(0,0,0,0.6)); filter: drop-shadow(0 0 20px ${terrainColor}bb) drop-shadow(0 12px 30px rgba(0,0,0,0.7));
"> ">
${islandSVG} ${islandSVG}
<!-- Island info card -->
<div style=" <div style="
position: absolute; position: absolute;
bottom: -18px; bottom: -35px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: rgba(20, 30, 50, 0.95); background: linear-gradient(135deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.95));
color: white; color: white;
padding: 2px 8px; padding: 6px 12px;
border-radius: 8px; border-radius: 12px;
font-size: 10px; font-size: 11px;
font-weight: 600; font-weight: 700;
white-space: nowrap; white-space: nowrap;
border: 1px solid rgba(255,255,255,0.25); border: 1px solid ${terrainColor}66;
text-shadow: 0 1px 2px rgba(0,0,0,0.5); text-shadow: 0 1px 2px rgba(0,0,0,0.5);
box-shadow: 0 2px 8px rgba(0,0,0,0.4); box-shadow: 0 4px 16px rgba(0,0,0,0.5), 0 0 12px ${terrainColor}33;
">@${island.username}</div> backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
">
<span style="color: ${terrainColor}; font-size: 12px;">@${island.username}</span>
<span style="font-size: 9px; color: rgba(255,255,255,0.7); font-weight: 500;">
${postCount} posts ${terrain}
</span>
</div>
</div> </div>
`; `;
@ -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) { function generateMiniIsland(seed, terrainColor) {
// Seeded random // Seeded random
@ -269,57 +290,127 @@ function generateMiniIsland(seed, terrainColor) {
return seed / 233280; return seed / 233280;
}; };
// Generate organic island shape using bezier curves // Much bigger island
const size = 50; const size = 120;
const centerX = size / 2; const centerX = size / 2;
const centerY = size / 2; const centerY = size / 2;
const numPoints = 8; const numPoints = 12;
let path = ''; let path = '';
const points = []; const points = [];
for (let i = 0; i < numPoints; i++) { for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2; const angle = (i / numPoints) * Math.PI * 2;
const radiusVariation = 0.7 + rng() * 0.6; // Random radius const radiusVariation = 0.65 + rng() * 0.5;
const radius = (size / 2) * radiusVariation; const radius = (size / 2.2) * radiusVariation;
const x = centerX + Math.cos(angle) * radius; const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius; const y = centerY + Math.sin(angle) * radius;
points.push({ x, y }); points.push({ x, y });
} }
// Create smooth curve using quadratic bezier
path = `M ${points[0].x} ${points[0].y}`; path = `M ${points[0].x} ${points[0].y}`;
for (let i = 0; i < numPoints; i++) { for (let i = 0; i < numPoints; i++) {
const current = points[i]; const current = points[i];
const next = points[(i + 1) % numPoints]; const next = points[(i + 1) % numPoints];
const controlX = (current.x + next.x) / 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) * 5; const controlY = (current.y + next.y) / 2 + (rng() - 0.5) * 8;
path += ` Q ${controlX} ${controlY} ${next.x} ${next.y}`; path += ` Q ${controlX} ${controlY} ${next.x} ${next.y}`;
} }
path += ' Z'; path += ' Z';
// Add some trees/details // Beach ring
const numTrees = 2 + Math.floor(rng() * 3); 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 = ''; let trees = '';
for (let i = 0; i < numTrees; i++) { for (let i = 0; i < numTrees; i++) {
const tx = centerX + (rng() - 0.5) * (size * 0.4); const angle = rng() * Math.PI * 2;
const ty = centerY + (rng() - 0.5) * (size * 0.4); const dist = 8 + rng() * 25;
const treeSize = 3 + rng() * 2; const tx = centerX + Math.cos(angle) * dist;
trees += `<circle cx="${tx}" cy="${ty}" r="${treeSize}" fill="rgba(0,80,0,0.6)" />`; const ty = centerY + Math.sin(angle) * dist;
const treeHeight = 8 + rng() * 10;
const treeWidth = 4 + rng() * 4;
// Palm tree shape
trees += `
<g transform="translate(${tx}, ${ty})">
<rect x="${-1}" y="${-treeHeight}" width="2" height="${treeHeight}" fill="#5D4037" rx="1"/>
<ellipse cx="0" cy="${-treeHeight}" rx="${treeWidth}" ry="${treeWidth * 0.6}" fill="#228B22" opacity="0.9"/>
<ellipse cx="${-treeWidth * 0.5}" cy="${-treeHeight + 2}" rx="${treeWidth * 0.7}" ry="${treeWidth * 0.4}" fill="#2E7D32" opacity="0.8"/>
</g>
`;
}
// 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 += `
<g transform="translate(${hx}, ${hy})">
<rect x="-5" y="-6" width="10" height="8" fill="#8D6E63" rx="1"/>
<polygon points="-7,-6 0,-12 7,-6" fill="#D32F2F"/>
</g>
`;
}
// Water sparkles
let sparkles = '';
for (let i = 0; i < 6; i++) {
const sx = rng() * size;
const sy = rng() * size;
sparkles += `<circle cx="${sx}" cy="${sy}" r="1.5" fill="white" opacity="${0.3 + rng() * 0.4}"/>`;
} }
return ` return `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="overflow: visible;"> <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="overflow: visible;">
<defs> <defs>
<radialGradient id="island-grad-${seed}" cx="35%" cy="35%"> <radialGradient id="island-grad-${seed}" cx="30%" cy="30%">
<stop offset="0%" stop-color="${terrainColor}" stop-opacity="1" /> <stop offset="0%" stop-color="${terrainColor}" stop-opacity="1" />
<stop offset="70%" stop-color="${terrainColor}" stop-opacity="0.9" /> <stop offset="60%" stop-color="${terrainColor}" stop-opacity="0.95" />
<stop offset="100%" stop-color="${terrainColor}" stop-opacity="0.7" /> <stop offset="100%" stop-color="${terrainColor}" stop-opacity="0.8" />
</radialGradient> </radialGradient>
<radialGradient id="beach-grad-${seed}" cx="50%" cy="50%">
<stop offset="0%" stop-color="#F5DEB3" stop-opacity="0.9" />
<stop offset="100%" stop-color="#DEB887" stop-opacity="0.7" />
</radialGradient>
<filter id="glow-${seed}">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs> </defs>
<!-- Island shape --> <!-- Ocean sparkles -->
<path d="${path}" fill="url(#island-grad-${seed})" stroke="rgba(255,255,255,0.3)" stroke-width="1.5" /> ${sparkles}
<!-- Trees/details --> <!-- Beach ring -->
<path d="${beachPath}" fill="url(#beach-grad-${seed})" stroke="rgba(255,255,255,0.2)" stroke-width="1" />
<!-- Main island -->
<path d="${path}" fill="url(#island-grad-${seed})" stroke="rgba(255,255,255,0.35)" stroke-width="2" filter="url(#glow-${seed})" />
<!-- Buildings -->
${huts}
<!-- Trees -->
${trees} ${trees}
</svg> </svg>
`; `;

View File

@ -491,6 +491,42 @@ export default function IslandViewer({ island, onClose }) {
</div> </div>
</div> </div>
{/* Action buttons */}
<div className="island-viewer-actions">
{isSelf && (
<button
type="button"
className="island-action-btn island-action-primary"
onClick={() => {
trackEvent("island_add_post_click", { username: islandUsername });
// Dispatch event to open create post modal
window.dispatchEvent(new CustomEvent('sociowire:create-post', {
detail: { island: islandUsername }
}));
onClose?.();
}}
>
<i className="fa-solid fa-plus" /> Add Post to Island
</button>
)}
<button
type="button"
className="island-action-btn"
onClick={() => {
trackEvent("island_share_click", { username: islandUsername });
const shareUrl = `${window.location.origin}/?island=${islandUsername}`;
if (navigator.share) {
navigator.share({ title: `${islandUsername}'s Island`, url: shareUrl });
} else {
navigator.clipboard?.writeText(shareUrl);
alert('Link copied!');
}
}}
>
<i className="fa-solid fa-share-nodes" /> Share Island
</button>
</div>
<div className="island-viewer-footer"> <div className="island-viewer-footer">
<p className="island-info"> <p className="island-info">
🌱 Seed: {island.seed} 🎨 {island.color_palette || 'vibrant'} palette 🌱 Seed: {island.seed} 🎨 {island.color_palette || 'vibrant'} palette
@ -605,37 +641,74 @@ function createIslandElement(seed, terrainType = 'tropical') {
el.className = 'island-terrain-marker'; el.className = 'island-terrain-marker';
const terrainColor = getTerrainColorCSS(terrainType); 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 = ` el.innerHTML = `
<div style=" <div style="
width: ${size}px; width: ${size}px;
height: ${size}px; height: ${size}px;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(circle at 30% 30%, ${terrainColor}ee, ${terrainColor}aa, ${terrainColor}77); background: radial-gradient(circle at 30% 30%, ${terrainColor}ee, ${terrainColor}cc, ${terrainColor}88);
box-shadow: box-shadow:
0 0 40px ${terrainColor}88, 0 0 60px ${terrainColor}99,
0 20px 40px rgba(0,0,0,0.4), 0 30px 60px rgba(0,0,0,0.5),
inset -10px -10px 30px rgba(0,0,0,0.3), inset -15px -15px 40px rgba(0,0,0,0.35),
inset 10px 10px 30px rgba(255,255,255,0.2); inset 15px 15px 40px rgba(255,255,255,0.25);
position: relative; position: relative;
animation: islandFloat 4s ease-in-out infinite; animation: islandFloat 4s ease-in-out infinite;
"> ">
<!-- Beach ring -->
<div style=" <div style="
position: absolute; position: absolute;
inset: 8px; inset: 12px;
border-radius: 50%; border-radius: 50%;
border: 6px solid rgba(255, 243, 199, 0.55); border: 10px solid rgba(255, 243, 199, 0.5);
box-shadow: inset 0 0 12px rgba(255, 243, 199, 0.45); box-shadow: inset 0 0 20px rgba(255, 243, 199, 0.4);
"></div> "></div>
<!-- Inner grass/terrain -->
<div style=" <div style="
position: absolute; position: absolute;
top: 12%; inset: 30px;
right: 18%; border-radius: 50%;
font-size: 22px; background: radial-gradient(circle at 40% 40%, ${terrainColor}dd, ${terrainColor}99);
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.4)); box-shadow: inset 0 0 30px rgba(0,0,0,0.2);
"></div>
<!-- Decorative elements -->
<div style="
position: absolute;
top: 15%;
right: 20%;
font-size: 32px;
filter: drop-shadow(0 6px 8px rgba(0,0,0,0.5));
animation: contentFloat 3s ease-in-out infinite; animation: contentFloat 3s ease-in-out infinite;
"></div> ">${terrainEmoji}</div>
<div style="
position: absolute;
bottom: 25%;
left: 18%;
font-size: 26px;
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.4));
animation: contentFloat 4s ease-in-out infinite;
animation-delay: 0.5s;
">${terrainEmoji}</div>
<div style="
position: absolute;
top: 40%;
left: 25%;
font-size: 20px;
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.4));
animation: contentFloat 3.5s ease-in-out infinite;
animation-delay: 1s;
">🏠</div>
${createIslandTexture(seed, terrainType)} ${createIslandTexture(seed, terrainType)}
</div> </div>
`; `;

View File

@ -1476,11 +1476,11 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
<a class="sw-cta-primary sw-cta-camera" href="${escapeHtml(normalizeMediaUrl(postData.video_url || postData.embed_url || ''))}" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;background:linear-gradient(135deg,#dc2626,#ef4444);"> <a class="sw-cta-primary sw-cta-camera" href="${escapeHtml(normalizeMediaUrl(postData.video_url || postData.embed_url || ''))}" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;background:linear-gradient(135deg,#dc2626,#ef4444);">
<i class="fa-solid fa-video"></i> Watch Live Stream <i class="fa-solid fa-video"></i> Watch Live Stream
</a> </a>
` : ` ` : url ? `
<button class="sw-cta-primary" type="button" ${url ? "" : "disabled"} data-url="${escapeHtml(url)}"> <a class="sw-cta-primary" href="${escapeHtml(url)}" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;">
Open source <i class="fa-solid fa-external-link"></i> Open source
</button> </a>
`} ` : ``}
<div class="sw-cta-actions"> <div class="sw-cta-actions">
${!isCamera ? `<button class="post-card-btn" type="button">Contact</button>` : ''} ${!isCamera ? `<button class="post-card-btn" type="button">Contact</button>` : ''}

View File

@ -688,3 +688,71 @@ body[data-theme="light"] .password-modal-field input {
color: #1e293b; color: #1e293b;
border-color: rgba(99, 102, 241, 0.3); 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;
}