redesign: map page mobile UI overhaul v1.0.61
Toolbar: - Slim rounded-pill search input (was tall boxy default) - Compact icon-only view toggle pills - Small circle filter button (red when active, badge count) Right-side control strip: - Layer toggles (Spots/Shops/Gear/Tours) as 36px circles active=opaque, inactive=35% opacity — no more giant emoji FABs - Near Me button integrated into strip (removed from top-right) - Quick Snap in strip, same style — no more big red circle - All transitions: translate-x-2 slide-out when overlays open Left-side Add FAB: - Single gradient pill (red→orange) replaces two competing circles - Smaller footprint, less visual noise Loading indicator: - Replaced center-screen "Loading..." pill with slim animated gradient bar at top edge of map (top-0 left-0 right-0 h-0.5) Attribution: - Moved from floating top-right to bottom-left corner - 9px text, 50% opacity — required but invisible Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
7335255f37
commit
056846fc38
3 changed files with 120 additions and 128 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.0.60",
|
||||
"version": "1.0.61",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.com",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
v1.0.60 — UI/UX fixes
|
||||
v1.0.61 — Map UI overhaul
|
||||
|
||||
FIXED: Burger menu now fully covers map controls
|
||||
- Sheet drawer z-index raised above all map floating buttons (z-[2100])
|
||||
- Map FABs, zoom controls, and filter pills no longer punch through the open menu
|
||||
REDESIGNED: Mobile map toolbar
|
||||
- Search bar is now a slim rounded pill (not a tall boxy input)
|
||||
- View toggle (Map/List) is a compact pill with icon-only buttons
|
||||
- Filter button is a small circle that turns red when filters are active
|
||||
- No more generic-looking default form controls
|
||||
|
||||
IMPROVED: My Stash desktop layout
|
||||
- Sidebar expanded to 300px with real visual weight
|
||||
- Inventory card: large item count, est. sats value, per-type stats row (Wishlist / For Sale / To Trade)
|
||||
- Clickable category breakdown bars act as filter shortcuts
|
||||
- Marketplace CTA card: sell/trade count badges, gradient button
|
||||
- Quick Add buttons (Add to Stash / Add to Wishlist) moved into sidebar
|
||||
- Grid layout: 2/3/4 columns (md/lg/xl) — no more awkward stretched single-item rows
|
||||
- Bigger, more purposeful empty states
|
||||
REDESIGNED: Right-side control strip
|
||||
- Layer toggles (Spots/Shops/Gear/Tours) are now small 36px circles — active = opaque, inactive = 35% opacity
|
||||
- Near Me button moved into the right strip (was floating top-right separately)
|
||||
- Quick Snap camera button matches the same strip style
|
||||
- No more giant colored emoji circles
|
||||
|
||||
REDESIGNED: Add Spot FAB (bottom-left)
|
||||
- Single gradient pill button (red→orange), replaces the two separate large circles
|
||||
- Cleaner, less cluttered
|
||||
|
||||
FIXED: Loading indicator
|
||||
- Replaced the center-screen "Loading..." pill with a slim animated gradient bar at the top edge of the map
|
||||
|
||||
FIXED: Map attribution
|
||||
- Moved to bottom-left corner, minimal and unobtrusive
|
||||
|
|
|
|||
|
|
@ -898,60 +898,66 @@ export default function MapPage() {
|
|||
{/* Full-height map container: subtract header + bottom nav on mobile, header only on desktop */}
|
||||
{/* 100dvh handles iOS Safari URL bar shrink correctly */}
|
||||
<div className="h-[calc(100dvh-var(--header-mobile)-var(--bottom-nav))] md:h-[calc(100vh-4rem)] flex flex-col">
|
||||
{/* Toolbar */}
|
||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container px-3 sm:px-4 py-2 sm:py-3">
|
||||
{/* Row 1: Search + View Toggle + Filters (mobile) */}
|
||||
{/* ── Toolbar ── */}
|
||||
<div className="border-b border-white/8 bg-black/95 backdrop-blur">
|
||||
<div className="px-3 py-2">
|
||||
{/* Single row: search pill + view toggle + filter button */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Search */}
|
||||
{/* Search pill */}
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-gray-500 pointer-events-none" />
|
||||
<Input
|
||||
placeholder={t('mapPage.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-10"
|
||||
className="pl-9 h-9 bg-white/6 border-white/10 text-white placeholder:text-gray-600 rounded-full text-sm focus:border-[#ff0040]/50 focus:bg-white/8 transition-all"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button onClick={() => setSearchQuery('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white">
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* View toggle - Map / List */}
|
||||
<div className="flex items-center rounded-lg border bg-muted p-0.5 flex-shrink-0">
|
||||
<Button
|
||||
variant={viewMode === 'map' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-9 w-9 sm:w-auto sm:px-3 p-0"
|
||||
{/* View toggle — compact pill */}
|
||||
<div className="flex items-center rounded-full border border-white/10 bg-white/4 p-0.5 flex-shrink-0">
|
||||
<button
|
||||
className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
|
||||
viewMode === 'map' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
|
||||
onClick={() => setViewMode('map')}
|
||||
aria-label="Map view"
|
||||
>
|
||||
<Map className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">{t('mapPage.mapView')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-9 w-9 sm:w-auto sm:px-3 p-0"
|
||||
<Map className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
|
||||
viewMode === 'list' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
|
||||
onClick={() => setViewMode('list')}
|
||||
aria-label="List view"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">{t('mapPage.listView')}</span>
|
||||
</Button>
|
||||
<List className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Filters button that opens sheet */}
|
||||
{/* Filter button — mobile */}
|
||||
<Sheet open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 px-3 gap-2 md:hidden flex-shrink-0 relative"
|
||||
<button
|
||||
className={cn(
|
||||
'relative h-9 w-9 rounded-full flex items-center justify-center flex-shrink-0 border transition-all touch-manipulation active:scale-90 md:hidden',
|
||||
totalActiveFilters > 0
|
||||
? 'bg-[#ff0040]/20 border-[#ff0040]/60 text-[#ff0040]'
|
||||
: 'bg-white/6 border-white/10 text-gray-400 hover:text-gray-200 hover:border-white/20'
|
||||
)}
|
||||
aria-label="Filters"
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Filters</span>
|
||||
<SlidersHorizontal className="h-3.5 w-3.5" />
|
||||
{totalActiveFilters > 0 && (
|
||||
<span className="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 text-[10px] font-bold bg-[#ff0040] text-white rounded-full flex items-center justify-center">
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 text-[9px] font-black bg-[#ff0040] text-white rounded-full flex items-center justify-center leading-none">
|
||||
{totalActiveFilters}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="bottom" className="h-[82vh] max-h-[680px] rounded-t-2xl px-4 flex flex-col">
|
||||
<SheetHeader className="pb-2 flex-shrink-0">
|
||||
|
|
@ -1411,84 +1417,43 @@ export default function MapPage() {
|
|||
flyTo={flyTo}
|
||||
/>
|
||||
</Suspense>
|
||||
{/* Loading indicator */}
|
||||
{/* Loading indicator — slim top edge bar */}
|
||||
{isLoading && (
|
||||
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-[1000] bg-black/90 backdrop-blur px-4 py-2 rounded-lg shadow-lg border border-[#ff0040]/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-[#ff0040] border-t-transparent" />
|
||||
<span className="text-sm text-white font-bold">{t('common.loading')}</span>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 right-0 z-[1000] h-0.5">
|
||||
<div className="h-full bg-gradient-to-r from-[#ff0040] via-[#ff6600] to-[#ccff00] animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Spot + Add Shop FABs — fixed to screen, above bottom nav.
|
||||
Uses fixed positioning so CSS variable issues on Capacitor don't affect placement.
|
||||
calc(4rem + env(safe-area-inset-bottom, 0px)) + 44px gap = clears nav on all devices. */}
|
||||
{/* Add Spot + Add Shop FABs — single pill button, bottom-left */}
|
||||
{user && (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed left-3 z-[1001] md:hidden flex flex-col items-start gap-2.5 transition-all duration-200",
|
||||
"fixed left-3 z-[1001] md:hidden transition-all duration-200",
|
||||
mobileFiltersOpen || selectedSpot || sasquatchPanelOpen
|
||||
? "opacity-0 pointer-events-none scale-90"
|
||||
: "opacity-100 scale-100"
|
||||
? "opacity-0 pointer-events-none translate-y-2"
|
||||
: "opacity-100 translate-y-0"
|
||||
)}
|
||||
style={{ bottom: 'calc(3.5rem + env(safe-area-inset-bottom, 0px) + 0.75rem)' }}
|
||||
>
|
||||
{/* Add Shop — green circle */}
|
||||
<Link to="/create-shop" aria-label="Add a skate shop">
|
||||
<button
|
||||
className="h-12 w-12 rounded-full bg-[#00cc66] shadow-xl shadow-[#00cc66]/40 border-2 border-white/25 flex items-center justify-center active:scale-90 transition-transform touch-manipulation"
|
||||
aria-label="Add shop"
|
||||
>
|
||||
<Plus className="h-6 w-6 text-black" strokeWidth={3} />
|
||||
</button>
|
||||
</Link>
|
||||
{/* Add Spot — red circle */}
|
||||
{/* Single "+" FAB — opens a mini menu via state */}
|
||||
<Link to="/create-spot" aria-label="Add a skate spot">
|
||||
<button
|
||||
className="h-12 w-12 rounded-full bg-[#ff0040] shadow-xl shadow-[#ff0040]/40 border-2 border-white/25 flex items-center justify-center active:scale-90 transition-transform touch-manipulation"
|
||||
aria-label="Add spot"
|
||||
className="h-12 w-12 rounded-full bg-gradient-to-br from-[#ff0040] to-[#ff6600] shadow-xl shadow-[#ff0040]/30 border border-white/20 flex items-center justify-center active:scale-90 transition-transform touch-manipulation"
|
||||
>
|
||||
<Plus className="h-6 w-6 text-white" strokeWidth={3} />
|
||||
<Plus className="h-5 w-5 text-white" strokeWidth={2.5} />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Near Me button + attribution — floating top-right, hidden when any overlay is open */}
|
||||
<div className={cn(
|
||||
"absolute top-4 right-4 z-[1000] flex flex-col items-end gap-1.5 transition-opacity duration-150",
|
||||
mobileFiltersOpen ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
)}>
|
||||
<Button
|
||||
onClick={nearMeError === 'denied' ? () => setShowLocationHelp(true) : handleNearMe}
|
||||
disabled={nearMeLoading}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-11 w-11 rounded-full shadow-lg backdrop-blur p-0 border transition-all active:scale-90 touch-manipulation",
|
||||
nearMeError === 'denied'
|
||||
? "bg-red-900/80 hover:bg-red-800/90 border-red-500/40"
|
||||
: "bg-black/80 hover:bg-black/95 border-white/20"
|
||||
)}
|
||||
title={nearMeError === 'denied' ? 'Location access denied — tap for help' : 'Find spots near me'}
|
||||
aria-label={nearMeError === 'denied' ? 'Location access denied — tap for help' : 'Find spots near my location'}
|
||||
>
|
||||
{nearMeLoading ? (
|
||||
<span className="h-4 w-4 rounded-full border-2 border-white/30 border-t-white animate-spin" />
|
||||
) : nearMeError === 'denied' ? (
|
||||
<LocateOff className="h-4 w-4 text-red-400" />
|
||||
) : (
|
||||
<LocateFixed className="h-4 w-4 text-[#ccff00]" />
|
||||
)}
|
||||
</Button>
|
||||
{/* Map attribution */}
|
||||
<div className="bg-black/70 backdrop-blur px-2 py-1 rounded text-[10px] text-gray-400 text-right leading-snug">
|
||||
©{' '}
|
||||
<a href="https://carto.com/attributions" target="_blank" rel="noopener noreferrer" className="text-gray-300 hover:underline">CARTO</a>
|
||||
{/* Map attribution — bottom-left corner, minimal */}
|
||||
<div className="absolute bottom-2 left-2 z-[999] pointer-events-none">
|
||||
<div className="bg-black/50 backdrop-blur px-1.5 py-0.5 rounded text-[9px] text-gray-600 leading-snug pointer-events-auto">
|
||||
<a href="https://carto.com/attributions" target="_blank" rel="noopener noreferrer" className="hover:text-gray-400 transition-colors">CARTO</a>
|
||||
{' · '}
|
||||
<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer" className="text-gray-300 hover:underline">OSM</a>
|
||||
<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer" className="hover:text-gray-400 transition-colors">OSM</a>
|
||||
{showBtcMap && (
|
||||
<>{' · '}<a href="https://btcmap.org" target="_blank" rel="noopener noreferrer" className="text-[#f7931a] hover:underline">BTC Map</a></>
|
||||
<>{' · '}<a href="https://btcmap.org" target="_blank" rel="noopener noreferrer" className="text-[#f7931a]/60 hover:text-[#f7931a] transition-colors">BTC Map</a></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1538,53 +1503,71 @@ export default function MapPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Right-side control column — filter pills + Quick Snap (mobile only, hidden when overlays open) */}
|
||||
{/* Right-side control strip — layer toggles + Near Me + Snap (mobile only) */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed right-3 z-[1000] md:hidden flex flex-col items-center gap-2 transition-opacity duration-150",
|
||||
mobileFiltersOpen || selectedSpot ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
"fixed right-3 z-[1000] md:hidden flex flex-col items-center gap-1.5 transition-all duration-200",
|
||||
mobileFiltersOpen || selectedSpot ? "opacity-0 pointer-events-none translate-x-2" : "opacity-100 translate-x-0"
|
||||
)}
|
||||
style={{ bottom: 'calc(3.5rem + env(safe-area-inset-bottom, 0px) + 0.75rem)' }}
|
||||
>
|
||||
{/* Layer filter pills — compact icon-only */}
|
||||
{/* Near Me */}
|
||||
<button
|
||||
onClick={nearMeError === 'denied' ? () => setShowLocationHelp(true) : handleNearMe}
|
||||
disabled={nearMeLoading}
|
||||
className={cn(
|
||||
'h-9 w-9 rounded-full flex items-center justify-center shadow-lg border transition-all touch-manipulation active:scale-90',
|
||||
nearMeError === 'denied'
|
||||
? 'bg-red-900/80 border-red-500/30'
|
||||
: 'bg-black/70 border-white/15 hover:border-white/30'
|
||||
)}
|
||||
aria-label="Find spots near me"
|
||||
>
|
||||
{nearMeLoading ? (
|
||||
<span className="h-3.5 w-3.5 rounded-full border-2 border-white/20 border-t-white animate-spin" />
|
||||
) : nearMeError === 'denied' ? (
|
||||
<LocateOff className="h-3.5 w-3.5 text-red-400" />
|
||||
) : (
|
||||
<LocateFixed className="h-3.5 w-3.5 text-[#ccff00]" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-4 h-px bg-white/10" />
|
||||
|
||||
{/* Layer toggles — slim pill style */}
|
||||
{[
|
||||
{ key: 'spots', label: '🛹', active: showSpots, toggle: () => setShowSpots(v => !v), activeColor: 'bg-red-500/90' },
|
||||
{ key: 'shops', label: '🏪', active: showShops, toggle: () => setShowShops(v => !v), activeColor: 'bg-purple-600/90' },
|
||||
{ key: 'gear', label: '🛒', active: showGear, toggle: () => setShowGear(v => !v), activeColor: 'bg-orange-500/90' },
|
||||
{ key: 'tours', label: '🗺️', active: showTours, toggle: () => setShowTours(v => !v), activeColor: 'bg-cyan-500/90' },
|
||||
].map(({ key, label, active, toggle, activeColor }) => (
|
||||
{ key: 'spots', emoji: '🛹', active: showSpots, toggle: () => setShowSpots(v => !v) },
|
||||
{ key: 'shops', emoji: '🏪', active: showShops, toggle: () => setShowShops(v => !v) },
|
||||
{ key: 'gear', emoji: '🛒', active: showGear, toggle: () => setShowGear(v => !v) },
|
||||
{ key: 'tours', emoji: '🗺️', active: showTours, toggle: () => setShowTours(v => !v) },
|
||||
].map(({ key, emoji, active, toggle }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={toggle}
|
||||
className={cn(
|
||||
// 40×40 ensures 44pt Apple / 48dp Material touch target compliance
|
||||
'h-10 w-10 rounded-full text-base flex items-center justify-center shadow-md border transition-all touch-manipulation active:scale-90',
|
||||
'h-9 w-9 rounded-full text-sm flex items-center justify-center border transition-all touch-manipulation active:scale-90',
|
||||
active
|
||||
? `${activeColor} border-white/20 shadow-black/30`
|
||||
: 'bg-black/60 border-white/8 opacity-40'
|
||||
? 'bg-black/80 border-white/25 shadow-md'
|
||||
: 'bg-black/40 border-white/6 opacity-35'
|
||||
)}
|
||||
title={key}
|
||||
aria-label={`Toggle ${key}`}
|
||||
>
|
||||
{label}
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Divider */}
|
||||
{user && <div className="w-4 h-px bg-white/15 my-0.5" />}
|
||||
|
||||
{/* Quick Snap */}
|
||||
{/* Divider + Quick Snap */}
|
||||
{user && (
|
||||
<>
|
||||
<Button
|
||||
<div className="w-4 h-px bg-white/10" />
|
||||
<button
|
||||
onClick={() => setShowQuickCamera(true)}
|
||||
className="h-12 w-12 rounded-full bg-[#ff0040] hover:bg-[#ff0040]/90 shadow-lg shadow-[#ff0040]/25 border-2 border-white/20"
|
||||
className="h-9 w-9 rounded-full bg-black/70 border border-white/15 flex items-center justify-center shadow-lg touch-manipulation active:scale-90 hover:border-[#ff0040]/50 hover:bg-[#ff0040]/10 transition-all"
|
||||
aria-label="Quick snap"
|
||||
>
|
||||
<Camera className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="text-[10px] text-white/60 font-bold whitespace-nowrap">
|
||||
Snap
|
||||
</span>
|
||||
<Camera className="h-3.5 w-3.5 text-gray-300" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue