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:
parent
fa345f23fd
commit
6e29a502d4
|
|
@ -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 = `
|
||||
<div class="universe-island-shape" style="
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: universeIslandFloat 6s ease-in-out infinite;
|
||||
animation-delay: ${idx * 0.5}s;
|
||||
filter: drop-shadow(0 0 15px ${terrainColor}aa) drop-shadow(0 8px 20px rgba(0,0,0,0.6));
|
||||
animation-delay: ${(idx % 10) * 0.3}s;
|
||||
filter: drop-shadow(0 0 20px ${terrainColor}bb) drop-shadow(0 12px 30px rgba(0,0,0,0.7));
|
||||
">
|
||||
${islandSVG}
|
||||
<!-- Island info card -->
|
||||
<div style="
|
||||
position: absolute;
|
||||
bottom: -18px;
|
||||
bottom: -35px;
|
||||
left: 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;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
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);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||
">@${island.username}</div>
|
||||
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;
|
||||
">
|
||||
<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>
|
||||
`;
|
||||
|
||||
|
|
@ -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 += `<circle cx="${tx}" cy="${ty}" r="${treeSize}" fill="rgba(0,80,0,0.6)" />`;
|
||||
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 += `
|
||||
<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 `
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="overflow: visible;">
|
||||
<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="70%" stop-color="${terrainColor}" stop-opacity="0.9" />
|
||||
<stop offset="100%" stop-color="${terrainColor}" stop-opacity="0.7" />
|
||||
<stop offset="60%" stop-color="${terrainColor}" stop-opacity="0.95" />
|
||||
<stop offset="100%" stop-color="${terrainColor}" stop-opacity="0.8" />
|
||||
</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>
|
||||
<!-- Island shape -->
|
||||
<path d="${path}" fill="url(#island-grad-${seed})" stroke="rgba(255,255,255,0.3)" stroke-width="1.5" />
|
||||
<!-- Trees/details -->
|
||||
<!-- Ocean sparkles -->
|
||||
${sparkles}
|
||||
<!-- 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}
|
||||
</svg>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -491,6 +491,42 @@ export default function IslandViewer({ island, onClose }) {
|
|||
</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">
|
||||
<p className="island-info">
|
||||
🌱 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 = `
|
||||
<div style="
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
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:
|
||||
0 0 40px ${terrainColor}88,
|
||||
0 20px 40px rgba(0,0,0,0.4),
|
||||
inset -10px -10px 30px rgba(0,0,0,0.3),
|
||||
inset 10px 10px 30px rgba(255,255,255,0.2);
|
||||
0 0 60px ${terrainColor}99,
|
||||
0 30px 60px rgba(0,0,0,0.5),
|
||||
inset -15px -15px 40px rgba(0,0,0,0.35),
|
||||
inset 15px 15px 40px rgba(255,255,255,0.25);
|
||||
position: relative;
|
||||
animation: islandFloat 4s ease-in-out infinite;
|
||||
">
|
||||
<!-- Beach ring -->
|
||||
<div style="
|
||||
position: absolute;
|
||||
inset: 8px;
|
||||
inset: 12px;
|
||||
border-radius: 50%;
|
||||
border: 6px solid rgba(255, 243, 199, 0.55);
|
||||
box-shadow: inset 0 0 12px rgba(255, 243, 199, 0.45);
|
||||
border: 10px solid rgba(255, 243, 199, 0.5);
|
||||
box-shadow: inset 0 0 20px rgba(255, 243, 199, 0.4);
|
||||
"></div>
|
||||
<!-- Inner grass/terrain -->
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 12%;
|
||||
right: 18%;
|
||||
font-size: 22px;
|
||||
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.4));
|
||||
inset: 30px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 40% 40%, ${terrainColor}dd, ${terrainColor}99);
|
||||
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;
|
||||
">✨</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)}
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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);">
|
||||
<i class="fa-solid fa-video"></i> Watch Live Stream
|
||||
</a>
|
||||
` : `
|
||||
<button class="sw-cta-primary" type="button" ${url ? "" : "disabled"} data-url="${escapeHtml(url)}">
|
||||
Open source
|
||||
</button>
|
||||
`}
|
||||
` : 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;">
|
||||
<i class="fa-solid fa-external-link"></i> Open source
|
||||
</a>
|
||||
` : ``}
|
||||
|
||||
<div class="sw-cta-actions">
|
||||
${!isCamera ? `<button class="post-card-btn" type="button">Contact</button>` : ''}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue