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:
shakespeare.diy 2026-07-24 01:49:07 +07:00
parent 7335255f37
commit 056846fc38
3 changed files with 120 additions and 128 deletions

View file

@ -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": {

View file

@ -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

View file

@ -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>