diff --git a/package.json b/package.json index 9836e2c..896afb4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sociowire-frontend", "private": true, - "version": "0.0.116", + "version": "0.0.117", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index d86d3d9..1f6c4db 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -2200,10 +2200,10 @@ export default function MapView({ )} - -
- -
- + {showList && (
diff --git a/src/components/World/IslandsWorld.css b/src/components/World/IslandsWorld.css index ed675cd..96603be 100644 --- a/src/components/World/IslandsWorld.css +++ b/src/components/World/IslandsWorld.css @@ -7,6 +7,8 @@ padding: 0; background: linear-gradient(180deg, #0a1628 0%, #0d2847 50%, #1e3a5f 100%); overflow: hidden; + /* Prevent browser zoom on touch devices - only our map should zoom */ + touch-action: pan-x pan-y; } .islands-world-canvas { @@ -403,6 +405,10 @@ z-index: 700; background: #0a1628; 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 { @@ -506,20 +512,72 @@ 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 { position: absolute; inset: 0; top: 70px; - cursor: crosshair; + cursor: grab; 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; - inset: -10%; - width: 120%; - height: 120%; + inset: 0; + transform-origin: center center; + 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 */ diff --git a/src/components/World/IslandsWorld.jsx b/src/components/World/IslandsWorld.jsx index 0c4246d..2155576 100644 --- a/src/components/World/IslandsWorld.jsx +++ b/src/components/World/IslandsWorld.jsx @@ -34,6 +34,12 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl const [pendingPostPosition, setPendingPostPosition] = useState(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) useEffect(() => { if (zoomedIsland) { @@ -105,8 +111,108 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl setZoomedIsland(null); setIslandPosts([]); setIsZooming(false); + setTerrainTransform({ x: 0, y: 0, scale: 1 }); // Reset transform }, [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 useEffect(() => { if (!containerRef.current || loading) return; @@ -316,98 +422,147 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
- {/* Interactive terrain area */} + {/* Interactive terrain area - map-like pan/zoom */}
{ + // Don't place post if we just finished dragging + if (isDraggingTerrain.current) return; + // Only owner can place posts if (!username || username.toLowerCase() !== (zoomedIsland.username || '').toLowerCase()) return; const rect = terrainRef.current?.getBoundingClientRect(); if (!rect) return; - // Calculate position as percentage (0-100) - const x = ((e.clientX - rect.left) / rect.width) * 100; - const y = ((e.clientY - rect.top) / rect.height) * 100; + // Calculate position as percentage (0-100), accounting for transform + const rawX = e.clientX - rect.left - rect.width / 2 - terrainTransform.x; + 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 }); }} > - {/* Full-screen island terrain background */} - + {/* Transformable content container (pan/zoom like a map) */} +
+ {/* Full-screen island terrain background */} + - {/* Loading */} - {loadingPosts && ( -
-
-
- )} + {/* Loading */} + {loadingPosts && ( +
+
+
+ )} - {/* Empty state hint */} - {!loadingPosts && islandPosts.length === 0 && !pendingPostPosition && ( -
- -

{t('worlds.emptyIsland')}

- {username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && ( -

{t('worlds.clickToPlacePost')}

- )} -
- )} + {/* Empty state hint */} + {!loadingPosts && islandPosts.length === 0 && !pendingPostPosition && ( +
+ +

{t('worlds.emptyIsland')}

+ {username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && ( +

{t('worlds.clickToPlacePost')}

+ )} +
+ )} - {/* Posts displayed at their coordinates */} - {!loadingPosts && islandPosts.map((post, idx) => ( - - ))} + {/* Posts displayed at their coordinates */} + {!loadingPosts && islandPosts.map((post, idx) => ( + + ))} - {/* Pending post marker (where user clicked) */} - {pendingPostPosition && ( -
+
+ +
+
+ + +
+
+ )} +
{/* End transform container */} + + {/* Zoom controls */} +
+ - -
-
- )} + + + + +
)} @@ -478,7 +633,7 @@ function createIslandMarker(island, idx, t, currentUsername) { */ function generateIslandSVG(seed, color) { const rng = seededRandom(seed); - const size = 140; + const size = 200; // Larger islands for better visibility const cx = size / 2; const cy = size / 2; const numPoints = 10; @@ -633,7 +788,7 @@ function IslandTerrainBackground({ island }) { */ function IslandShape({ seed, cx, cy, scale, fill, opacity = 1, filter }) { const rng = seededRandom(seed); - const baseRadius = 30; + const baseRadius = 38; // Larger island to fill more space const numPoints = 12; const points = []; diff --git a/src/styles/base.css b/src/styles/base.css index d00b648..d2357e1 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1,7 +1,35 @@ * { box-sizing: border-box; } +html { + /* Prevent zoom/scaling on mobile inputs */ + touch-action: manipulation; +} body { margin: 0; background: #050816; color: #ffffff; 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; + } } diff --git a/src/styles/smartSearchBar.css b/src/styles/smartSearchBar.css index 1fdb4a0..52f8764 100644 --- a/src/styles/smartSearchBar.css +++ b/src/styles/smartSearchBar.css @@ -75,6 +75,7 @@ body[data-theme="light"] { background: var(--sw-panel-bg, rgba(15, 18, 28, 0.62)); border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10)); backdrop-filter: blur(10px); + overflow: visible; } .sw-search__bar{ @@ -92,7 +93,8 @@ body[data-theme="light"] { } .sw-search__input { - flex: 1; + flex: 1 1 auto; + min-width: 0; border: 0; outline: 0; background: transparent; @@ -102,6 +104,13 @@ body[data-theme="light"] { 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 { color: var(--sw-muted, rgba(255,255,255,0.55)); } @@ -117,6 +126,8 @@ body[data-theme="light"] { align-items: center; justify-content: center; transition: color 0.15s ease; + flex-shrink: 0; + margin-right: 4px; } .sw-search__clearBtn:hover { @@ -132,11 +143,7 @@ body[data-theme="light"] { align-items: center; gap: 6px; flex-shrink: 0; - margin-left: -6px; -} - -.sw-search__icons .sw-search__iconBtn:first-child{ - margin-left: -2px; + margin-left: 4px; } .sw-search__iconBtn { @@ -144,9 +151,9 @@ body[data-theme="light"] { outline: 0; cursor: pointer; - width: 34px; - height: 34px; - border-radius: 12px; + width: 32px; + height: 32px; + border-radius: 8px; display: inline-flex; align-items: center; @@ -154,6 +161,17 @@ body[data-theme="light"] { background: var(--sw-chip-bg, rgba(255,255,255,0.08)); 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 {