Skip to content

Commit d2c5a64

Browse files
authored
Merge pull request #8 from digidem/claude/mobile-map-screen-ui-fixes-01HXT2XitKy6KhGwHC6F5Wc5
Mobile map screen UI layout fixes
2 parents 46d01db + edb3bd6 commit d2c5a64

File tree

14 files changed

+205
-80
lines changed

14 files changed

+205
-80
lines changed

.github/workflows/deploy-pr-preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
- name: Setup Node.js
5050
uses: actions/setup-node@v4
5151
with:
52-
node-version: '18'
52+
node-version: '20'
5353
cache: 'npm'
5454

5555
- name: Install dependencies

.github/workflows/deploy-production.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Setup Node.js
3434
uses: actions/setup-node@v4
3535
with:
36-
node-version: '18'
36+
node-version: '20'
3737
cache: 'npm'
3838

3939
- name: Install dependencies

src/components/CoordinateDisplay.tsx

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Button } from "@/components/ui/button";
2+
import { BottomSheet } from "@/components/ui/bottom-sheet";
23
import { MapPin, X } from "lucide-react";
34
import { useTranslation } from "react-i18next";
5+
import { useIsMobile } from "@/hooks/use-mobile";
46
import { Coordinates } from "@/types/common";
57

68
interface CoordinateDisplayProps {
@@ -15,48 +17,67 @@ export const CoordinateDisplay = ({
1517
onCancel,
1618
}: CoordinateDisplayProps) => {
1719
const { t } = useTranslation();
20+
const isMobile = useIsMobile();
1821

22+
// Content component shared between mobile and desktop
23+
const content = (
24+
<div className="space-y-4">
25+
<div className="flex items-center gap-3">
26+
<div className="w-12 h-12 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
27+
<MapPin className="w-6 h-6 text-white" />
28+
</div>
29+
<div className="flex-1 min-w-0">
30+
<p className="font-semibold text-gray-800 text-lg">
31+
{t("map.selectedLocation")}
32+
</p>
33+
<p
34+
className="text-sm text-gray-600 truncate"
35+
role="region"
36+
aria-label="Selected coordinates"
37+
>
38+
<span>Lat: {coordinates.lat}</span>,{" "}
39+
<span>Lng: {coordinates.lng}</span>
40+
</p>
41+
</div>
42+
</div>
43+
<div className="flex gap-3 pt-2">
44+
<Button
45+
onClick={onCancel}
46+
variant="outline"
47+
className="flex-1 h-12 min-w-[44px]"
48+
aria-label="Cancel"
49+
>
50+
<X className="w-4 h-4 mr-2" />
51+
{t("common.cancel")}
52+
</Button>
53+
<Button
54+
onClick={onContinue}
55+
className="flex-1 bg-green-600 hover:bg-green-700 h-12 font-medium"
56+
aria-label="Continue to project selection"
57+
>
58+
{t("map.continue")}
59+
</Button>
60+
</div>
61+
</div>
62+
);
63+
64+
// Mobile: Bottom sheet
65+
if (isMobile) {
66+
return (
67+
<BottomSheet isOpen={true} onClose={onCancel}>
68+
{content}
69+
</BottomSheet>
70+
);
71+
}
72+
73+
// Desktop: Floating card
1974
return (
2075
<div
2176
className="absolute left-4 right-4 z-10 md:left-6 md:right-auto md:w-auto"
2277
style={{ bottom: `max(80px, calc(env(safe-area-inset-bottom) + 80px))` }}
2378
>
2479
<div className="bg-white/95 backdrop-blur-sm rounded-2xl shadow-lg p-4 animate-fade-in">
25-
<div className="flex items-center gap-3">
26-
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
27-
<MapPin className="w-5 h-5 text-white" />
28-
</div>
29-
<div className="flex-1 min-w-0">
30-
<p className="font-semibold text-gray-800 text-base">
31-
{t("map.selectedLocation")}
32-
</p>
33-
<p
34-
className="text-sm text-gray-600 truncate"
35-
role="region"
36-
aria-label="Selected coordinates"
37-
>
38-
<span>Lat: {coordinates.lat}</span>,{" "}
39-
<span>Lng: {coordinates.lng}</span>
40-
</p>
41-
</div>
42-
<div className="flex gap-2">
43-
<Button
44-
onClick={onCancel}
45-
variant="outline"
46-
className="h-12 px-4 min-w-[44px]"
47-
aria-label="Cancel"
48-
>
49-
<X className="w-4 h-4" />
50-
</Button>
51-
<Button
52-
onClick={onContinue}
53-
className="bg-green-600 hover:bg-green-700 h-12 px-6 font-medium min-w-[100px]"
54-
aria-label="Continue to project selection"
55-
>
56-
{t("map.continue")}
57-
</Button>
58-
</div>
59-
</div>
80+
{content}
6081
</div>
6182
</div>
6283
);

src/components/MapInterface.tsx

Lines changed: 108 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useState, useEffect, useCallback, useRef } from "react";
22
import { Button } from "@/components/ui/button";
3-
import { LogOut, Settings, Download } from "lucide-react";
3+
import { LogOut, Settings, Download, Search } from "lucide-react";
44
import { toast } from "sonner";
55
import { usePWAInstall } from "@/hooks/usePWAInstall";
6+
import { useIsMobile } from "@/hooks/use-mobile";
67
import { AlertPopup } from "@/components/AlertPopup";
78
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
89
import { MapTokenSetup } from "@/components/MapTokenSetup";
@@ -11,6 +12,7 @@ import { CoordinateDisplay } from "@/components/CoordinateDisplay";
1112
import { ManualCoordinateEntry } from "@/components/ManualCoordinateEntry";
1213
import { MapContainer } from "@/components/MapContainer";
1314
import { ProjectSelector } from "@/components/ProjectSelector";
15+
import { BottomSheet } from "@/components/ui/bottom-sheet";
1416
import { useMapAlerts } from "@/hooks/useMapAlerts";
1517
import { useMapSearch } from "@/hooks/useMapSearch";
1618
import { 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+
3643
export 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 */}

src/components/ProjectSelection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export const ProjectSelection = ({
224224

225225
<Button
226226
onClick={handleContinue}
227-
className="w-full mt-6 h-12 text-base"
227+
className="w-full mt-6 min-h-12 py-3 text-sm sm:text-base whitespace-normal"
228228
disabled={selectedProjects.length === 0}
229229
>
230230
{t("projects.continueToAlert", {

src/components/ProjectSelector.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ export const ProjectSelector = ({
4848
variant="outline"
4949
size="sm"
5050
disabled={isLoading}
51-
className="flex items-center gap-1 h-11 min-w-[44px] max-w-[200px] md:max-w-none"
51+
className="flex items-center gap-1 sm:gap-1.5 h-9 sm:h-11 min-w-[44px] flex-1 max-w-[280px] sm:max-w-[320px] md:max-w-none md:flex-none"
5252
>
53-
<FolderOpen className="w-4 h-4 flex-shrink-0" />
54-
<span className="hidden sm:inline truncate">
53+
<FolderOpen className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
54+
<span className="truncate text-xs sm:text-sm font-medium">
5555
{selectedProject?.name || t("projects.selectProject")}
5656
</span>
5757
<ChevronDown className="w-3 h-3 flex-shrink-0" />

0 commit comments

Comments
 (0)