11import { useState , useEffect , useCallback , useRef } from "react" ;
22import { Button } from "@/components/ui/button" ;
3- import { LogOut , Settings , Download } from "lucide-react" ;
3+ import { LogOut , Settings , Download , Search } from "lucide-react" ;
44import { toast } from "sonner" ;
55import { usePWAInstall } from "@/hooks/usePWAInstall" ;
6+ import { useIsMobile } from "@/hooks/use-mobile" ;
67import { AlertPopup } from "@/components/AlertPopup" ;
78import { LanguageSwitcher } from "@/components/LanguageSwitcher" ;
89import { MapTokenSetup } from "@/components/MapTokenSetup" ;
@@ -11,6 +12,7 @@ import { CoordinateDisplay } from "@/components/CoordinateDisplay";
1112import { ManualCoordinateEntry } from "@/components/ManualCoordinateEntry" ;
1213import { MapContainer } from "@/components/MapContainer" ;
1314import { ProjectSelector } from "@/components/ProjectSelector" ;
15+ import { BottomSheet } from "@/components/ui/bottom-sheet" ;
1416import { useMapAlerts } from "@/hooks/useMapAlerts" ;
1517import { useMapSearch } from "@/hooks/useMapSearch" ;
1618import { useMapInteraction } from "@/hooks/useMapInteraction" ;
@@ -33,6 +35,11 @@ interface MapInterfaceProps {
3335 isLoadingProjects ?: boolean ;
3436}
3537
38+ // Navbar height constants for consistent positioning
39+ const NAVBAR_HEIGHT_MOBILE = 56 ; // Approximate height on mobile (py-2 + safe-area)
40+ const NAVBAR_HEIGHT_DESKTOP = 64 ; // Approximate height on desktop (py-3)
41+ const BUTTON_TOP_OFFSET = 16 ; // Gap between navbar and floating buttons
42+
3643export const MapInterface = ( {
3744 onCoordinatesSet,
3845 onLogout,
@@ -42,10 +49,12 @@ export const MapInterface = ({
4249 isLoadingProjects = false ,
4350} : MapInterfaceProps ) => {
4451 const { t } = useTranslation ( ) ;
52+ const isMobile = useIsMobile ( ) ;
4553 const [ selectedCoords , setSelectedCoords ] = useState < Coordinates | null > (
4654 coordinates ,
4755 ) ;
4856 const [ showManualEntry , setShowManualEntry ] = useState ( false ) ;
57+ const [ showSearchModal , setShowSearchModal ] = useState ( false ) ;
4958 // Initialize mapboxToken directly from environment to avoid async timing issues
5059 const [ mapboxToken , setMapboxToken ] = useState ( ( ) => {
5160 const envToken = import . meta. env . VITE_MAPBOX_TOKEN ;
@@ -66,6 +75,11 @@ export const MapInterface = ({
6675
6776 const { isInstallable, installApp } = usePWAInstall ( ) ;
6877
78+ // Calculate button positions based on navbar height
79+ const floatingButtonTop = isMobile
80+ ? NAVBAR_HEIGHT_MOBILE + BUTTON_TOP_OFFSET
81+ : NAVBAR_HEIGHT_DESKTOP + BUTTON_TOP_OFFSET ;
82+
6983 // Initialize selected project when projects first load (only once)
7084 useEffect ( ( ) => {
7185 if ( projects . length > 0 && ! projectInitializedRef . current ) {
@@ -138,6 +152,26 @@ export const MapInterface = ({
138152 } ;
139153 } , [ cleanupMarkers ] ) ;
140154
155+ // Adjust map padding on mobile when coordinates are selected to keep marker visible above bottom sheet
156+ useEffect ( ( ) => {
157+ const map = mapInstanceRef . current ;
158+ if ( ! map || ! isMapLoaded ) return ;
159+
160+ if ( isMobile && selectedCoords ) {
161+ // Add padding to bottom to account for bottom sheet (approximately 200px)
162+ map . easeTo ( {
163+ padding : { top : 0 , bottom : 250 , left : 0 , right : 0 } ,
164+ duration : 300 ,
165+ } ) ;
166+ } else if ( isMobile && ! selectedCoords ) {
167+ // Reset padding when coordinates are cleared
168+ map . easeTo ( {
169+ padding : { top : 0 , bottom : 0 , left : 0 , right : 0 } ,
170+ duration : 300 ,
171+ } ) ;
172+ }
173+ } , [ selectedCoords , isMobile , isMapLoaded ] ) ;
174+
141175 const handleTokenSubmit = ( ) => {
142176 setShowTokenInput ( false ) ;
143177 } ;
@@ -227,34 +261,35 @@ export const MapInterface = ({
227261 searchInputRef = { searchInputRef }
228262 />
229263
230- { /* Mobile-optimized floating header with safe area */ }
231- < div className = "absolute top-0 left-0 right-0 z-10 bg-white/95 backdrop-blur-sm border-b border-gray-200" >
264+ { /* Single-row navbar with safe area */ }
265+ < div className = "absolute top-0 left-0 right-0 z-30 bg-white/95 backdrop-blur-sm border-b border-gray-200" >
232266 < div
233- className = "flex justify-between items-center px-4 py-3 h-16 "
234- style = { { paddingTop : "max(0.75rem , env(safe-area-inset-top))" } }
267+ className = "flex justify-between items-center px-2 sm:px- 4 py-2 sm:py-3 "
268+ style = { { paddingTop : "max(0.5rem , env(safe-area-inset-top))" } }
235269 >
236- < div className = "flex items-center gap-3 flex-1 min-w-0" >
237- < h1 className = "text-lg font-bold text-gray-800 hidden md:block" >
238- { t ( "app.title" ) }
239- </ h1 >
240- < h1 className = "text-sm font-bold text-gray-800 md:hidden truncate" >
241- { t ( "app.title" ) }
242- </ h1 >
270+ < div className = "flex items-center gap-1.5 sm:gap-3 flex-1 min-w-0" >
271+ { /* App logo - hidden on very small screens */ }
272+ < img
273+ src = "/icon.svg"
274+ alt = "CoMapeo Logo"
275+ className = "hidden xs:block w-8 h-8 flex-shrink-0"
276+ />
277+ { /* Project selector - shows selected project name */ }
243278 < ProjectSelector
244279 projects = { projects }
245280 selectedProject = { selectedProject }
246281 onProjectSelect = { handleProjectSelect }
247282 isLoading = { isLoadingProjects || isLoadingAlerts }
248283 />
249284 </ div >
250- < div className = "flex items-center gap-2 flex-shrink-0" >
285+ < div className = "flex items-center gap-1 sm:gap- 2 flex-shrink-0" >
251286 < LanguageSwitcher />
252287 { isInstallable && (
253288 < Button
254289 variant = "outline"
255290 size = "sm"
256291 onClick = { installApp }
257- className = "flex items-center gap-1 h-11 min-w-[44px]"
292+ className = "flex items-center gap-1 h-9 sm:h- 11 min-w-[44px]"
258293 aria-label = { t ( "common.installApp" ) }
259294 >
260295 < Download className = "w-4 h-4" />
@@ -265,7 +300,7 @@ export const MapInterface = ({
265300 variant = "outline"
266301 size = "sm"
267302 onClick = { onLogout }
268- className = "flex items-center gap-1 h-11 min-w-[44px]"
303+ className = "flex items-center gap-1 h-9 sm:h- 11 min-w-[44px]"
269304 aria-label = { t ( "projects.logout" ) }
270305 >
271306 < LogOut className = "w-4 h-4" />
@@ -275,30 +310,73 @@ export const MapInterface = ({
275310 </ div >
276311 </ div >
277312
278- { /* Enhanced search bar with recent searches */ }
279- < SearchBar
280- searchQuery = { searchQuery }
281- setSearchQuery = { setSearchQuery }
282- isSearching = { isSearching }
283- recentSearches = { recentSearches }
284- searchInputRef = { searchInputRef }
285- onSearch = { handleSearch }
286- onClearSearch = { handleClearSearch }
287- onRecentSearchClick = { handleRecentSearchClick }
288- />
313+ { /* Search UI - Different on mobile vs desktop */ }
314+ { isMobile ? (
315+ /* Mobile: Compact search trigger button */
316+ < div className = "absolute left-2 sm:left-4 z-20" style = { { top : `${ floatingButtonTop } px` } } >
317+ < Button
318+ onClick = { ( ) => setShowSearchModal ( true ) }
319+ className = "w-12 h-12 rounded-full bg-white/95 backdrop-blur-sm shadow-lg hover:bg-white border border-gray-200"
320+ aria-label = "Open search"
321+ >
322+ < Search className = "w-5 h-5 text-gray-700" />
323+ </ Button >
324+ </ div >
325+ ) : (
326+ /* Desktop: Full search bar always visible */
327+ < SearchBar
328+ searchQuery = { searchQuery }
329+ setSearchQuery = { setSearchQuery }
330+ isSearching = { isSearching }
331+ recentSearches = { recentSearches }
332+ searchInputRef = { searchInputRef }
333+ onSearch = { handleSearch }
334+ onClearSearch = { handleClearSearch }
335+ onRecentSearchClick = { handleRecentSearchClick }
336+ />
337+ ) }
338+
339+ { /* Mobile: Search bottom sheet */ }
340+ { isMobile && (
341+ < BottomSheet
342+ isOpen = { showSearchModal }
343+ onClose = { ( ) => setShowSearchModal ( false ) }
344+ title = { t ( "map.searchPlaceholder" ) }
345+ >
346+ < SearchBar
347+ searchQuery = { searchQuery }
348+ setSearchQuery = { setSearchQuery }
349+ isSearching = { isSearching }
350+ recentSearches = { recentSearches }
351+ searchInputRef = { searchInputRef }
352+ onSearch = { ( ) => {
353+ handleSearch ( ) ;
354+ setShowSearchModal ( false ) ;
355+ } }
356+ onClearSearch = { handleClearSearch }
357+ onRecentSearchClick = { ( search ) => {
358+ handleRecentSearchClick ( search ) ;
359+ setShowSearchModal ( false ) ;
360+ } }
361+ standalone = { false }
362+ />
363+ </ BottomSheet >
364+ ) }
289365
290- { /* Mobile FAB for manual entry - larger touch target */ }
291- < div className = "absolute top-36 right-4 z-10 md:top-24" >
366+ { /* Floating map controls - vertically stacked on right side */ }
367+ < div className = "absolute right-2 sm:right-4 z-10 flex flex-col gap-3" style = { { top : `${ floatingButtonTop } px` } } >
368+ { /* Manual coordinate entry button */ }
292369 < Button
293370 onClick = { ( ) => setShowManualEntry ( true ) }
294- className = "w-14 h-14 rounded-full bg-blue-600 hover:bg-blue-700 shadow-lg md:w-auto md:h-auto md:rounded-lg md:px-4 md:py-2"
371+ className = "w-12 h-12 sm:w- 14 sm: h-14 rounded-full bg-blue-600 hover:bg-blue-700 shadow-lg md:w-auto md:h-auto md:rounded-lg md:px-4 md:py-2"
295372 aria-label = "Manual coordinate entry"
296373 >
297- < Settings className = "w-6 h-6 md:w-4 md:h-4" />
374+ < Settings className = "w-5 h-5 sm:w-6 sm: h-6 md:w-4 md:h-4" />
298375 < span className = "hidden md:inline md:ml-2" >
299376 { t ( "map.manualEntry" ) }
300377 </ span >
301378 </ Button >
379+ { /* Add more map controls here in the future if needed */ }
302380 </ div >
303381
304382 { /* Enhanced bottom sheet for manual entry */ }
0 commit comments