v112: Scatter islands randomly + full-screen terrain zoom with posts
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
b6b176271b
commit
6422aeebfe
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "sociowire-frontend",
|
"name": "sociowire-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.111",
|
"version": "0.0.112",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|
|
||||||
|
|
@ -366,26 +366,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Island Zoomed Overlay */
|
/* Island Terrain Overlay - Full screen when zoomed */
|
||||||
.island-zoomed-overlay {
|
.island-terrain-overlay {
|
||||||
position: fixed;
|
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;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 500;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.island-zoomed-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
background: linear-gradient(180deg, rgba(10, 22, 40, 0.95) 0%, rgba(10, 22, 40, 0) 100%);
|
background: linear-gradient(180deg, rgba(10, 22, 40, 0.95) 0%, rgba(10, 22, 40, 0) 100%);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-back {
|
.island-terrain-back {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
@ -398,41 +410,41 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-back:hover {
|
.island-terrain-back:hover {
|
||||||
background: rgba(56, 189, 248, 0.2);
|
background: rgba(56, 189, 248, 0.2);
|
||||||
border-color: rgba(56, 189, 248, 0.5);
|
border-color: rgba(56, 189, 248, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-info {
|
.island-terrain-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-info h2 {
|
.island-terrain-title h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #38bdf8;
|
color: #38bdf8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-info span {
|
.island-terrain-title span {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-actions {
|
.island-terrain-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-action {
|
.island-terrain-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px 16px;
|
padding: 10px 14px;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
@ -440,23 +452,161 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-action:hover {
|
.island-terrain-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-post {
|
.island-terrain-post-btn {
|
||||||
background: linear-gradient(135deg, #10b981, #059669);
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-zoomed-post:hover {
|
.island-terrain-post-btn:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
|
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 */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.islands-world-title {
|
.islands-world-title {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
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 { useAuth } from '../../auth/AuthContext';
|
||||||
import './IslandsWorld.css';
|
import './IslandsWorld.css';
|
||||||
|
|
||||||
|
|
@ -27,6 +27,8 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
// Zoomed island state
|
// Zoomed island state
|
||||||
const [zoomedIsland, setZoomedIsland] = useState(null);
|
const [zoomedIsland, setZoomedIsland] = useState(null);
|
||||||
const [isZooming, setIsZooming] = useState(false);
|
const [isZooming, setIsZooming] = useState(false);
|
||||||
|
const [islandPosts, setIslandPosts] = useState([]);
|
||||||
|
const [loadingPosts, setLoadingPosts] = useState(false);
|
||||||
|
|
||||||
// Fetch islands
|
// Fetch islands
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -56,8 +58,8 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
)
|
)
|
||||||
: islands;
|
: islands;
|
||||||
|
|
||||||
// Handle zoom to island
|
// Handle zoom to island - fills the screen with the island terrain
|
||||||
const handleZoomToIsland = useCallback((island) => {
|
const handleZoomToIsland = useCallback(async (island) => {
|
||||||
if (!mapRef.current || isZooming) return;
|
if (!mapRef.current || isZooming) return;
|
||||||
|
|
||||||
const pos = islandPositionsRef.current.get(island.username || island.id);
|
const pos = islandPositionsRef.current.get(island.username || island.id);
|
||||||
|
|
@ -65,6 +67,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
|
|
||||||
setIsZooming(true);
|
setIsZooming(true);
|
||||||
setZoomedIsland(island);
|
setZoomedIsland(island);
|
||||||
|
setLoadingPosts(true);
|
||||||
|
|
||||||
// Stop the rotation animation
|
// Stop the rotation animation
|
||||||
if (animationRef.current) {
|
if (animationRef.current) {
|
||||||
|
|
@ -72,16 +75,26 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
animationRef.current = null;
|
animationRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fly to island position with high zoom
|
// Fly to island - zoom high enough so island fills screen
|
||||||
mapRef.current.flyTo({
|
mapRef.current.flyTo({
|
||||||
center: [pos.lng, pos.lat],
|
center: [pos.lng, pos.lat],
|
||||||
zoom: 6,
|
zoom: 16,
|
||||||
pitch: 60,
|
pitch: 0,
|
||||||
bearing: 0,
|
bearing: 0,
|
||||||
duration: 1500,
|
duration: 2000,
|
||||||
essential: true
|
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', () => {
|
mapRef.current.once('moveend', () => {
|
||||||
setIsZooming(false);
|
setIsZooming(false);
|
||||||
});
|
});
|
||||||
|
|
@ -92,6 +105,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
if (!mapRef.current || isZooming) return;
|
if (!mapRef.current || isZooming) return;
|
||||||
|
|
||||||
setIsZooming(true);
|
setIsZooming(true);
|
||||||
|
setIslandPosts([]);
|
||||||
|
|
||||||
mapRef.current.flyTo({
|
mapRef.current.flyTo({
|
||||||
center: [0, 0],
|
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 markers = [];
|
||||||
const gridSize = Math.ceil(Math.sqrt(filteredIslands.length || 1));
|
const worldSize = 300; // Large world area
|
||||||
const spacing = 20;
|
|
||||||
|
|
||||||
filteredIslands.forEach((island, idx) => {
|
filteredIslands.forEach((island, idx) => {
|
||||||
const gridX = idx % gridSize;
|
// Use seeded random for consistent positions per island
|
||||||
const gridY = Math.floor(idx / gridSize);
|
const rng = seededRandom((island.username || island.id || '').charCodeAt(0) * 1000 + idx);
|
||||||
const baseX = (gridX - gridSize / 2) * spacing;
|
// Random position across the entire world
|
||||||
const baseY = (gridY - gridSize / 2) * spacing;
|
const lng = (rng() - 0.5) * worldSize;
|
||||||
const jitterX = (Math.random() - 0.5) * spacing * 0.4;
|
const lat = (rng() - 0.5) * worldSize;
|
||||||
const jitterY = (Math.random() - 0.5) * spacing * 0.4;
|
|
||||||
const lng = centerLng + baseX + jitterX;
|
|
||||||
const lat = centerLat + baseY + jitterY;
|
|
||||||
|
|
||||||
// Store position for zoom
|
// Store position for zoom
|
||||||
islandPositionsRef.current.set(island.username || island.id, { lng, lat });
|
islandPositionsRef.current.set(island.username || island.id, { lng, lat });
|
||||||
|
|
@ -301,34 +311,68 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Zoomed island UI overlay */}
|
{/* Zoomed island - full screen terrain view */}
|
||||||
{zoomedIsland && !isZooming && (
|
{zoomedIsland && (
|
||||||
<div className="island-zoomed-overlay">
|
<div className={`island-terrain-overlay ${isZooming ? 'zooming' : 'ready'}`}>
|
||||||
<div className="island-zoomed-header">
|
{/* Header bar */}
|
||||||
<button className="island-zoomed-back" onClick={handleZoomOut}>
|
<div className="island-terrain-header">
|
||||||
|
<button className="island-terrain-back" onClick={handleZoomOut}>
|
||||||
<i className="fa-solid fa-arrow-left" />
|
<i className="fa-solid fa-arrow-left" />
|
||||||
<span>{t('worlds.backToIslands')}</span>
|
<span>{t('worlds.backToIslands')}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="island-zoomed-info">
|
<div className="island-terrain-title">
|
||||||
<h2>@{zoomedIsland.username}</h2>
|
<h2>@{zoomedIsland.username}</h2>
|
||||||
<span>{zoomedIsland.terrain_type || 'tropical'}</span>
|
<span>{zoomedIsland.display_name || zoomedIsland.terrain_type || 'Island'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="island-zoomed-actions">
|
<div className="island-terrain-actions">
|
||||||
<button className="island-zoomed-action" onClick={() => window.location.href = `/profile/${zoomedIsland.username}`}>
|
<button className="island-terrain-btn" onClick={() => window.location.href = `/profile/${zoomedIsland.username}`}>
|
||||||
<i className="fa-solid fa-user" />
|
<i className="fa-solid fa-user" />
|
||||||
</button>
|
</button>
|
||||||
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
||||||
<button className="island-zoomed-action island-zoomed-post" onClick={() => {
|
<>
|
||||||
window.dispatchEvent(new CustomEvent('sociowire:create-post', {
|
<button className="island-terrain-btn" onClick={() => window.location.href = `/island/${zoomedIsland.username}/edit`}>
|
||||||
detail: { island: zoomedIsland.username }
|
<i className="fa-solid fa-cog" />
|
||||||
}));
|
</button>
|
||||||
}}>
|
<button className="island-terrain-btn island-terrain-post-btn" onClick={() => {
|
||||||
<i className="fa-solid fa-plus" />
|
window.dispatchEvent(new CustomEvent('sociowire:create-post', {
|
||||||
<span>{t('worlds.postHere')}</span>
|
detail: { island: zoomedIsland.username }
|
||||||
</button>
|
}));
|
||||||
|
}}>
|
||||||
|
<i className="fa-solid fa-plus" />
|
||||||
|
<span>{t('worlds.postHere')}</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Posts on the island terrain */}
|
||||||
|
<div className="island-terrain-posts">
|
||||||
|
{loadingPosts && (
|
||||||
|
<div className="island-terrain-loading">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loadingPosts && islandPosts.length === 0 && (
|
||||||
|
<div className="island-terrain-empty">
|
||||||
|
<i className="fa-solid fa-island-tropical" />
|
||||||
|
<p>{t('worlds.emptyIsland')}</p>
|
||||||
|
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
||||||
|
<button className="island-terrain-first-post" onClick={() => {
|
||||||
|
window.dispatchEvent(new CustomEvent('sociowire:create-post', {
|
||||||
|
detail: { island: zoomedIsland.username }
|
||||||
|
}));
|
||||||
|
}}>
|
||||||
|
<i className="fa-solid fa-plus" />
|
||||||
|
{t('worlds.createFirstPost')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loadingPosts && islandPosts.map((post, idx) => (
|
||||||
|
<IslandPost key={post.id || idx} post={post} index={idx} islandSeed={zoomedIsland.seed || 0} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -495,3 +539,44 @@ function seededRandom(seed) {
|
||||||
return s / 233280;
|
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 (
|
||||||
|
<div
|
||||||
|
className="island-post-marker"
|
||||||
|
style={{
|
||||||
|
left: `${left}%`,
|
||||||
|
top: `${top}%`,
|
||||||
|
transform: `rotate(${rotation}deg)`
|
||||||
|
}}
|
||||||
|
onClick={() => window.location.href = `/post/${post.id}`}
|
||||||
|
>
|
||||||
|
{hasImage ? (
|
||||||
|
<div className="island-post-image">
|
||||||
|
<img src={imageUrl} alt="" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="island-post-text">
|
||||||
|
<p>{(post.content || post.text || '').slice(0, 80)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="island-post-meta">
|
||||||
|
<span className="island-post-likes">
|
||||||
|
<i className="fa-solid fa-heart" /> {post.like_count || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue