feat: Islands v116 - Search bar fixes and island post improvements
- Fix search bar: single X button, form submit for Enter key - Fix search clearing to properly refetch normal posts - Hide native browser search cancel button with CSS - Add island post support with island_username context - Interactive island terrain with click-to-place posts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
90e9f79fcd
commit
3e780268ba
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "sociowire-frontend",
|
"name": "sociowire-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.116",
|
"version": "0.0.117",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|
|
||||||
|
|
@ -2200,10 +2200,10 @@ export default function MapView({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="sw-search__iconBtn"
|
className="sw-search__iconBtn"
|
||||||
title="New Post"
|
title="My Position"
|
||||||
onClick={() => handleOpenCreate && handleOpenCreate()}
|
onClick={handleFlyToMe}
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-plus"></i>
|
<i className="fa-solid fa-location-crosshairs"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -1343,71 +1343,91 @@ export function usePostsEngine({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [viewParams, mapReady, mainFilter, mainFilters.join(","), subFilter, subFilters.join(","), contentFilter, mapRef, timeHours, expandedElRef, refreshVisibleAndMarkers, computeVisible, onAutoWidenTimeFilter, syncMarkers, username]);
|
}, [viewParams, mapReady, mainFilter, mainFilters.join(","), subFilter, subFilters.join(","), contentFilter, mapRef, timeHours, expandedElRef, refreshVisibleAndMarkers, computeVisible, onAutoWidenTimeFilter, syncMarkers, username]);
|
||||||
|
|
||||||
|
// Quand searchQuery change, chercher via API dans le viewport actuel
|
||||||
|
// Track previous search to detect when search is cleared
|
||||||
|
const prevSearchRef = useRef("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const q = (searchQuery || "").trim();
|
const q = (searchQuery || "").trim();
|
||||||
if (!q) return;
|
const prevQ = prevSearchRef.current;
|
||||||
|
prevSearchRef.current = q;
|
||||||
|
|
||||||
|
if (!q) {
|
||||||
|
// Search was cleared - force refetch of normal posts
|
||||||
|
if (prevQ) {
|
||||||
|
// Had a search before, now cleared -> trigger full refetch
|
||||||
|
console.log('[usePostsEngine] Search cleared, forcing refetch');
|
||||||
|
searchResultsRef.current = false;
|
||||||
|
allPostsRef.current = [];
|
||||||
|
replacePoolRef.current = true;
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (map) {
|
||||||
|
map.__swForceFetchAt = Date.now();
|
||||||
|
setTimeout(() => {
|
||||||
|
try { map.fire("moveend"); } catch {}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark that we're showing search results
|
||||||
|
searchResultsRef.current = true;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function runSearch() {
|
async function runSearch() {
|
||||||
try {
|
try {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
let lat;
|
if (!map) return;
|
||||||
let lon;
|
|
||||||
let radiusKm;
|
// Obtenir le viewport actuel
|
||||||
try {
|
const center = map.getCenter();
|
||||||
const view = map ? getViewFromMap(map) : null;
|
const bounds = map.getBounds();
|
||||||
if (view?.center) {
|
let radiusKm = 50; // default
|
||||||
lon = view.center[0];
|
|
||||||
lat = view.center[1];
|
if (bounds) {
|
||||||
} else {
|
const sw = bounds.getSouthWest();
|
||||||
const center = map?.getCenter?.();
|
const ne = bounds.getNorthEast();
|
||||||
lat = center?.lat;
|
// Calculer le rayon approximatif du viewport
|
||||||
lon = center?.lng;
|
const latDiff = Math.abs(ne.lat - sw.lat);
|
||||||
|
const lonDiff = Math.abs(ne.lng - sw.lng);
|
||||||
|
radiusKm = Math.max(latDiff, lonDiff) * 111 / 2; // ~111km per degree
|
||||||
|
radiusKm = Math.min(Math.max(radiusKm, 10), 2000);
|
||||||
}
|
}
|
||||||
if (view?.radiusKm) {
|
|
||||||
radiusKm = Math.min(Math.max(view.radiusKm * 1.2, 25), 2500);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
const data = await fetchUnifiedSearch(q, {
|
const data = await fetchUnifiedSearch(q, {
|
||||||
type: "posts",
|
type: "posts",
|
||||||
lat,
|
lat: center?.lat,
|
||||||
lon,
|
lon: center?.lng,
|
||||||
radius_km: radiusKm,
|
radius_km: radiusKm,
|
||||||
limit: 60,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
const posts = Array.isArray(data?.posts) ? data.posts : [];
|
const posts = Array.isArray(data?.posts) ? data.posts : [];
|
||||||
const normalized = posts.map((p) => normalizePostId(p));
|
const normalized = posts.map((p) => normalizePostId(p));
|
||||||
let bounds = null;
|
|
||||||
try {
|
// Remplacer les posts par les résultats de recherche
|
||||||
const b = map?.getBounds?.();
|
allPostsRef.current = normalized;
|
||||||
if (b) {
|
refreshVisibleAndMarkers(timeHours, true);
|
||||||
const sw = b.getSouthWest();
|
|
||||||
const ne = b.getNorthEast();
|
trackEvent("search_viewport", {
|
||||||
bounds = { minLat: sw?.lat, minLon: sw?.lng, maxLat: ne?.lat, maxLon: ne?.lng };
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
allPostsRef.current = prunePosts(normalized, [lon, lat], MAX_POOL_POSTS, bounds);
|
|
||||||
searchResultsRef.current = true;
|
|
||||||
refreshVisibleAndMarkers(timeHours);
|
|
||||||
trackEvent("search_filter_apply", {
|
|
||||||
query_len: q.length,
|
query_len: q.length,
|
||||||
results: posts.length,
|
results: posts.length,
|
||||||
context: "map",
|
radius_km: Math.round(radiusKm),
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch (err) {
|
||||||
|
console.error('[usePostsEngine] Search error:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runSearch();
|
runSearch();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [searchQuery, viewParams, mapRef, refreshVisibleAndMarkers, timeHours]);
|
}, [searchQuery, mapRef, refreshVisibleAndMarkers, timeHours]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ((searchQuery || "").trim()) return;
|
|
||||||
searchResultsRef.current = false;
|
|
||||||
}, [searchQuery]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
|
|
|
||||||
|
|
@ -250,9 +250,10 @@ export default function PostComposer({ onClose, onPostCreated, islandUsername, i
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-full sm:max-w-lg max-h-[90vh] overflow-hidden rounded-t-3xl sm:rounded-3xl
|
className="w-full sm:max-w-lg max-h-[85vh] max-h-[85dvh] overflow-hidden rounded-t-3xl sm:rounded-3xl
|
||||||
bg-gradient-to-b from-surface-800 to-surface-900
|
bg-gradient-to-b from-surface-800 to-surface-900
|
||||||
border border-white/10 shadow-2xl"
|
border border-white/10 shadow-2xl"
|
||||||
|
style={{ maxHeight: 'min(85vh, 85dvh)' }}
|
||||||
initial={{ y: "100%", opacity: 0 }}
|
initial={{ y: "100%", opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
exit={{ y: "100%", opacity: 0 }}
|
exit={{ y: "100%", opacity: 0 }}
|
||||||
|
|
@ -288,7 +289,7 @@ export default function PostComposer({ onClose, onPostCreated, islandUsername, i
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-4 overflow-y-auto max-h-[calc(90vh-140px)]">
|
<div className="p-4 overflow-y-auto" style={{ maxHeight: 'calc(85dvh - 140px)' }}>
|
||||||
{/* User info */}
|
{/* User info */}
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center text-white text-lg font-bold">
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center text-white text-lg font-bold">
|
||||||
|
|
|
||||||
|
|
@ -242,11 +242,25 @@ export default function SmartSearchBar({
|
||||||
onSearch && onSearch(""); // Clear filters
|
onSearch && onSearch(""); // Clear filters
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (q.trim()) {
|
||||||
|
skipSearchRef.current = true;
|
||||||
|
setOpen(false);
|
||||||
|
trackEvent("search_submit", { query_len: q.trim().length });
|
||||||
|
onSearch && onSearch(q.trim());
|
||||||
|
if (inputRef.current) {
|
||||||
|
try { inputRef.current.blur(); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sw-search" ref={boxRef}>
|
<div className="sw-search" ref={boxRef}>
|
||||||
<div className="sw-search__bar">
|
<form className="sw-search__bar" onSubmit={handleFormSubmit}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
type="search"
|
||||||
className="sw-search__input"
|
className="sw-search__input"
|
||||||
value={q}
|
value={q}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -294,21 +308,7 @@ export default function SmartSearchBar({
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</form>
|
||||||
<div className="sw-search__icons" aria-label="Search actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sw-search__iconBtn"
|
|
||||||
title={t('map.mySpot')}
|
|
||||||
onClick={() => {
|
|
||||||
trackEvent("search_my_spot");
|
|
||||||
onMySpot && onMySpot();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconPin />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showList && (
|
{showList && (
|
||||||
<div className="sw-search__dropdown">
|
<div className="sw-search__dropdown">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: linear-gradient(180deg, #0a1628 0%, #0d2847 50%, #1e3a5f 100%);
|
background: linear-gradient(180deg, #0a1628 0%, #0d2847 50%, #1e3a5f 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Prevent browser zoom on touch devices - only our map should zoom */
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
}
|
}
|
||||||
|
|
||||||
.islands-world-canvas {
|
.islands-world-canvas {
|
||||||
|
|
@ -403,6 +405,10 @@
|
||||||
z-index: 700;
|
z-index: 700;
|
||||||
background: #0a1628;
|
background: #0a1628;
|
||||||
animation: islandFadeIn 0.4s ease;
|
animation: islandFadeIn 0.4s ease;
|
||||||
|
/* Prevent browser zoom - only our island terrain should zoom */
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes islandFadeIn {
|
@keyframes islandFadeIn {
|
||||||
|
|
@ -506,20 +512,72 @@
|
||||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
|
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interactive terrain area - full screen clickable */
|
/* Interactive terrain area - full screen with map-like navigation */
|
||||||
.island-terrain-interactive {
|
.island-terrain-interactive {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
top: 70px;
|
top: 70px;
|
||||||
cursor: crosshair;
|
cursor: grab;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Prevent browser zoom */
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.island-terrain-interactive .island-terrain-bg {
|
.island-terrain-interactive:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transform container for pan/zoom */
|
||||||
|
.island-terrain-transform {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -10%;
|
inset: 0;
|
||||||
width: 120%;
|
transform-origin: center center;
|
||||||
height: 120%;
|
will-change: transform;
|
||||||
|
transition: none; /* No transition for smooth dragging */
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-terrain-transform .island-terrain-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zoom controls (like map controls) */
|
||||||
|
.island-terrain-zoom-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-zoom-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(15, 23, 42, 0.9);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-zoom-btn:hover {
|
||||||
|
background: rgba(56, 189, 248, 0.3);
|
||||||
|
border-color: rgba(56, 189, 248, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-zoom-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Posts container - overlay for posts on island */
|
/* Posts container - overlay for posts on island */
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
const [pendingPostPosition, setPendingPostPosition] = useState(null);
|
const [pendingPostPosition, setPendingPostPosition] = useState(null);
|
||||||
const terrainRef = useRef(null);
|
const terrainRef = useRef(null);
|
||||||
|
|
||||||
|
// Island terrain pan/zoom state (map-like behavior)
|
||||||
|
const [terrainTransform, setTerrainTransform] = useState({ x: 0, y: 0, scale: 1 });
|
||||||
|
const isDraggingTerrain = useRef(false);
|
||||||
|
const lastPointerPos = useRef({ x: 0, y: 0 });
|
||||||
|
const lastPinchDist = useRef(0);
|
||||||
|
|
||||||
// Set global island context when zoomed (for FAB to use)
|
// Set global island context when zoomed (for FAB to use)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (zoomedIsland) {
|
if (zoomedIsland) {
|
||||||
|
|
@ -105,8 +111,108 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
setZoomedIsland(null);
|
setZoomedIsland(null);
|
||||||
setIslandPosts([]);
|
setIslandPosts([]);
|
||||||
setIsZooming(false);
|
setIsZooming(false);
|
||||||
|
setTerrainTransform({ x: 0, y: 0, scale: 1 }); // Reset transform
|
||||||
}, [isZooming]);
|
}, [isZooming]);
|
||||||
|
|
||||||
|
// Terrain pan handlers (map-like navigation)
|
||||||
|
const handleTerrainPointerDown = useCallback((e) => {
|
||||||
|
// Don't start drag if clicking on a post or button
|
||||||
|
if (e.target.closest('.island-post-marker, .island-pending-post-marker, button')) return;
|
||||||
|
|
||||||
|
isDraggingTerrain.current = true;
|
||||||
|
lastPointerPos.current = { x: e.clientX, y: e.clientY };
|
||||||
|
e.currentTarget.style.cursor = 'grabbing';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTerrainPointerMove = useCallback((e) => {
|
||||||
|
if (!isDraggingTerrain.current) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - lastPointerPos.current.x;
|
||||||
|
const dy = e.clientY - lastPointerPos.current.y;
|
||||||
|
lastPointerPos.current = { x: e.clientX, y: e.clientY };
|
||||||
|
|
||||||
|
setTerrainTransform(prev => ({
|
||||||
|
...prev,
|
||||||
|
x: prev.x + dx,
|
||||||
|
y: prev.y + dy
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTerrainPointerUp = useCallback((e) => {
|
||||||
|
isDraggingTerrain.current = false;
|
||||||
|
if (e.currentTarget) e.currentTarget.style.cursor = 'grab';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Terrain wheel zoom (map-like)
|
||||||
|
const handleTerrainWheel = useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const rect = terrainRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
|
||||||
|
// Mouse position relative to terrain center
|
||||||
|
const mouseX = e.clientX - rect.left - rect.width / 2;
|
||||||
|
const mouseY = e.clientY - rect.top - rect.height / 2;
|
||||||
|
|
||||||
|
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
|
|
||||||
|
setTerrainTransform(prev => {
|
||||||
|
const newScale = Math.max(0.5, Math.min(4, prev.scale * delta));
|
||||||
|
const scaleDiff = newScale / prev.scale;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scale: newScale,
|
||||||
|
x: prev.x * scaleDiff + mouseX * (1 - scaleDiff),
|
||||||
|
y: prev.y * scaleDiff + mouseY * (1 - scaleDiff)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Touch pinch zoom support
|
||||||
|
const handleTerrainTouchStart = useCallback((e) => {
|
||||||
|
if (e.touches.length === 2) {
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
lastPinchDist.current = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
} else if (e.touches.length === 1) {
|
||||||
|
isDraggingTerrain.current = true;
|
||||||
|
lastPointerPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTerrainTouchMove = useCallback((e) => {
|
||||||
|
if (e.touches.length === 2) {
|
||||||
|
e.preventDefault();
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (lastPinchDist.current > 0) {
|
||||||
|
const scaleDelta = dist / lastPinchDist.current;
|
||||||
|
setTerrainTransform(prev => ({
|
||||||
|
...prev,
|
||||||
|
scale: Math.max(0.5, Math.min(4, prev.scale * scaleDelta))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
lastPinchDist.current = dist;
|
||||||
|
} else if (e.touches.length === 1 && isDraggingTerrain.current) {
|
||||||
|
const dx = e.touches[0].clientX - lastPointerPos.current.x;
|
||||||
|
const dy = e.touches[0].clientY - lastPointerPos.current.y;
|
||||||
|
lastPointerPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||||
|
|
||||||
|
setTerrainTransform(prev => ({
|
||||||
|
...prev,
|
||||||
|
x: prev.x + dx,
|
||||||
|
y: prev.y + dy
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTerrainTouchEnd = useCallback(() => {
|
||||||
|
isDraggingTerrain.current = false;
|
||||||
|
lastPinchDist.current = 0;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Initialize MapLibre
|
// Initialize MapLibre
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || loading) return;
|
if (!containerRef.current || loading) return;
|
||||||
|
|
@ -316,23 +422,46 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Interactive terrain area */}
|
{/* Interactive terrain area - map-like pan/zoom */}
|
||||||
<div
|
<div
|
||||||
ref={terrainRef}
|
ref={terrainRef}
|
||||||
className="island-terrain-interactive"
|
className="island-terrain-interactive"
|
||||||
|
onPointerDown={handleTerrainPointerDown}
|
||||||
|
onPointerMove={handleTerrainPointerMove}
|
||||||
|
onPointerUp={handleTerrainPointerUp}
|
||||||
|
onPointerLeave={handleTerrainPointerUp}
|
||||||
|
onWheel={handleTerrainWheel}
|
||||||
|
onTouchStart={handleTerrainTouchStart}
|
||||||
|
onTouchMove={handleTerrainTouchMove}
|
||||||
|
onTouchEnd={handleTerrainTouchEnd}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
// Don't place post if we just finished dragging
|
||||||
|
if (isDraggingTerrain.current) return;
|
||||||
|
|
||||||
// Only owner can place posts
|
// Only owner can place posts
|
||||||
if (!username || username.toLowerCase() !== (zoomedIsland.username || '').toLowerCase()) return;
|
if (!username || username.toLowerCase() !== (zoomedIsland.username || '').toLowerCase()) return;
|
||||||
|
|
||||||
const rect = terrainRef.current?.getBoundingClientRect();
|
const rect = terrainRef.current?.getBoundingClientRect();
|
||||||
if (!rect) return;
|
if (!rect) return;
|
||||||
|
|
||||||
// Calculate position as percentage (0-100)
|
// Calculate position as percentage (0-100), accounting for transform
|
||||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
const rawX = e.clientX - rect.left - rect.width / 2 - terrainTransform.x;
|
||||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
const rawY = e.clientY - rect.top - rect.height / 2 - terrainTransform.y;
|
||||||
|
const x = (rawX / terrainTransform.scale / rect.width * 100) + 50;
|
||||||
|
const y = (rawY / terrainTransform.scale / rect.height * 100) + 50;
|
||||||
|
|
||||||
|
// Keep within bounds
|
||||||
|
if (x < 0 || x > 100 || y < 0 || y > 100) return;
|
||||||
|
|
||||||
setPendingPostPosition({ x, y });
|
setPendingPostPosition({ x, y });
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{/* Transformable content container (pan/zoom like a map) */}
|
||||||
|
<div
|
||||||
|
className="island-terrain-transform"
|
||||||
|
style={{
|
||||||
|
transform: `translate(${terrainTransform.x}px, ${terrainTransform.y}px) scale(${terrainTransform.scale})`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Full-screen island terrain background */}
|
{/* Full-screen island terrain background */}
|
||||||
<IslandTerrainBackground island={zoomedIsland} />
|
<IslandTerrainBackground island={zoomedIsland} />
|
||||||
|
|
@ -408,6 +537,32 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>{/* End transform container */}
|
||||||
|
|
||||||
|
{/* Zoom controls */}
|
||||||
|
<div className="island-terrain-zoom-controls">
|
||||||
|
<button
|
||||||
|
className="island-zoom-btn"
|
||||||
|
onClick={() => setTerrainTransform(prev => ({ ...prev, scale: Math.min(4, prev.scale * 1.3) }))}
|
||||||
|
title="Zoom in"
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-plus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="island-zoom-btn"
|
||||||
|
onClick={() => setTerrainTransform(prev => ({ ...prev, scale: Math.max(0.5, prev.scale / 1.3) }))}
|
||||||
|
title="Zoom out"
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-minus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="island-zoom-btn"
|
||||||
|
onClick={() => setTerrainTransform({ x: 0, y: 0, scale: 1 })}
|
||||||
|
title="Reset view"
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-expand" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -478,7 +633,7 @@ function createIslandMarker(island, idx, t, currentUsername) {
|
||||||
*/
|
*/
|
||||||
function generateIslandSVG(seed, color) {
|
function generateIslandSVG(seed, color) {
|
||||||
const rng = seededRandom(seed);
|
const rng = seededRandom(seed);
|
||||||
const size = 140;
|
const size = 200; // Larger islands for better visibility
|
||||||
const cx = size / 2;
|
const cx = size / 2;
|
||||||
const cy = size / 2;
|
const cy = size / 2;
|
||||||
const numPoints = 10;
|
const numPoints = 10;
|
||||||
|
|
@ -633,7 +788,7 @@ function IslandTerrainBackground({ island }) {
|
||||||
*/
|
*/
|
||||||
function IslandShape({ seed, cx, cy, scale, fill, opacity = 1, filter }) {
|
function IslandShape({ seed, cx, cy, scale, fill, opacity = 1, filter }) {
|
||||||
const rng = seededRandom(seed);
|
const rng = seededRandom(seed);
|
||||||
const baseRadius = 30;
|
const baseRadius = 38; // Larger island to fill more space
|
||||||
const numPoints = 12;
|
const numPoints = 12;
|
||||||
|
|
||||||
const points = [];
|
const points = [];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,35 @@
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
html {
|
||||||
|
/* Prevent zoom/scaling on mobile inputs */
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #050816;
|
background: #050816;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
/* Prevent horizontal scroll */
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global modal container fixes */
|
||||||
|
[class*="modal"],
|
||||||
|
[class*="Modal"],
|
||||||
|
[class*="overlay"],
|
||||||
|
[class*="Overlay"] {
|
||||||
|
/* Prevent modals from exceeding viewport */
|
||||||
|
max-height: 100vh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent zoom on input focus (iOS) */
|
||||||
|
input, select, textarea {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
input, select, textarea {
|
||||||
|
font-size: inherit !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ body[data-theme="light"] {
|
||||||
background: var(--sw-panel-bg, rgba(15, 18, 28, 0.62));
|
background: var(--sw-panel-bg, rgba(15, 18, 28, 0.62));
|
||||||
border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10));
|
border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10));
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-search__bar{
|
.sw-search__bar{
|
||||||
|
|
@ -92,7 +93,8 @@ body[data-theme="light"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-search__input {
|
.sw-search__input {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -102,6 +104,13 @@ body[data-theme="light"] {
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide native search clear button */
|
||||||
|
.sw-search__input::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sw-search__input::placeholder {
|
.sw-search__input::placeholder {
|
||||||
color: var(--sw-muted, rgba(255,255,255,0.55));
|
color: var(--sw-muted, rgba(255,255,255,0.55));
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +126,8 @@ body[data-theme="light"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-search__clearBtn:hover {
|
.sw-search__clearBtn:hover {
|
||||||
|
|
@ -132,11 +143,7 @@ body[data-theme="light"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: -6px;
|
margin-left: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.sw-search__icons .sw-search__iconBtn:first-child{
|
|
||||||
margin-left: -2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-search__iconBtn {
|
.sw-search__iconBtn {
|
||||||
|
|
@ -144,9 +151,9 @@ body[data-theme="light"] {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
width: 34px;
|
width: 32px;
|
||||||
height: 34px;
|
height: 32px;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -154,6 +161,17 @@ body[data-theme="light"] {
|
||||||
|
|
||||||
background: var(--sw-chip-bg, rgba(255,255,255,0.08));
|
background: var(--sw-chip-bg, rgba(255,255,255,0.08));
|
||||||
color: var(--sw-text, rgba(255,255,255,0.92));
|
color: var(--sw-text, rgba(255,255,255,0.92));
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-search__iconBtn:hover {
|
||||||
|
background: var(--sw-hover, rgba(255,255,255,0.15));
|
||||||
|
color: var(--sw-text, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* My Position button inside search bar */
|
||||||
|
.sw-search__mySpotBtn {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-search__iconBtn.is-active {
|
.sw-search__iconBtn.is-active {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue