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", "name": "skate-spots",
"private": true, "private": true,
"version": "1.0.60", "version": "1.0.61",
"type": "module", "type": "module",
"homepage": "https://skate-spots.com", "homepage": "https://skate-spots.com",
"scripts": { "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 REDESIGNED: Mobile map toolbar
- Sheet drawer z-index raised above all map floating buttons (z-[2100]) - Search bar is now a slim rounded pill (not a tall boxy input)
- Map FABs, zoom controls, and filter pills no longer punch through the open menu - 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 REDESIGNED: Right-side control strip
- Sidebar expanded to 300px with real visual weight - Layer toggles (Spots/Shops/Gear/Tours) are now small 36px circles — active = opaque, inactive = 35% opacity
- Inventory card: large item count, est. sats value, per-type stats row (Wishlist / For Sale / To Trade) - Near Me button moved into the right strip (was floating top-right separately)
- Clickable category breakdown bars act as filter shortcuts - Quick Snap camera button matches the same strip style
- Marketplace CTA card: sell/trade count badges, gradient button - No more giant colored emoji circles
- 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 REDESIGNED: Add Spot FAB (bottom-left)
- Bigger, more purposeful empty states - 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 */} {/* Full-height map container: subtract header + bottom nav on mobile, header only on desktop */}
{/* 100dvh handles iOS Safari URL bar shrink correctly */} {/* 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"> <div className="h-[calc(100dvh-var(--header-mobile)-var(--bottom-nav))] md:h-[calc(100vh-4rem)] flex flex-col">
{/* Toolbar */} {/* ── Toolbar ── */}
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="border-b border-white/8 bg-black/95 backdrop-blur">
<div className="container px-3 sm:px-4 py-2 sm:py-3"> <div className="px-3 py-2">
{/* Row 1: Search + View Toggle + Filters (mobile) */} {/* Single row: search pill + view toggle + filter button */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Search */} {/* Search pill */}
<div className="relative flex-1 min-w-0"> <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 <Input
placeholder={t('mapPage.searchPlaceholder')} placeholder={t('mapPage.searchPlaceholder')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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> </div>
{/* View toggle - Map / List */} {/* View toggle — compact pill */}
<div className="flex items-center rounded-lg border bg-muted p-0.5 flex-shrink-0"> <div className="flex items-center rounded-full border border-white/10 bg-white/4 p-0.5 flex-shrink-0">
<Button <button
variant={viewMode === 'map' ? 'default' : 'ghost'} className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
size="sm" viewMode === 'map' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
className="h-9 w-9 sm:w-auto sm:px-3 p-0"
onClick={() => setViewMode('map')} onClick={() => setViewMode('map')}
aria-label="Map view"
> >
<Map className="h-4 w-4" /> <Map className="h-3.5 w-3.5" />
<span className="hidden sm:inline ml-2">{t('mapPage.mapView')}</span> </button>
</Button> <button
<Button className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
variant={viewMode === 'list' ? 'default' : 'ghost'} viewMode === 'list' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
size="sm"
className="h-9 w-9 sm:w-auto sm:px-3 p-0"
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
aria-label="List view"
> >
<List className="h-4 w-4" /> <List className="h-3.5 w-3.5" />
<span className="hidden sm:inline ml-2">{t('mapPage.listView')}</span> </button>
</Button>
</div> </div>
{/* Mobile: Filters button that opens sheet */} {/* Filter button — mobile */}
<Sheet open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}> <Sheet open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <button
variant="outline" className={cn(
size="sm" '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',
className="h-10 px-3 gap-2 md:hidden flex-shrink-0 relative" 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" /> <SlidersHorizontal className="h-3.5 w-3.5" />
<span className="sr-only">Filters</span>
{totalActiveFilters > 0 && ( {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} {totalActiveFilters}
</span> </span>
)} )}
</Button> </button>
</SheetTrigger> </SheetTrigger>
<SheetContent side="bottom" className="h-[82vh] max-h-[680px] rounded-t-2xl px-4 flex flex-col"> <SheetContent side="bottom" className="h-[82vh] max-h-[680px] rounded-t-2xl px-4 flex flex-col">
<SheetHeader className="pb-2 flex-shrink-0"> <SheetHeader className="pb-2 flex-shrink-0">
@ -1411,84 +1417,43 @@ export default function MapPage() {
flyTo={flyTo} flyTo={flyTo}
/> />
</Suspense> </Suspense>
{/* Loading indicator */} {/* Loading indicator — slim top edge bar */}
{isLoading && ( {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="absolute top-0 left-0 right-0 z-[1000] h-0.5">
<div className="flex items-center gap-3"> <div className="h-full bg-gradient-to-r from-[#ff0040] via-[#ff6600] to-[#ccff00] animate-pulse" />
<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> </div>
)} )}
{/* Add Spot + Add Shop FABs — fixed to screen, above bottom nav. {/* Add Spot + Add Shop FABs — single pill button, bottom-left */}
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. */}
{user && ( {user && (
<div <div
className={cn( 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 mobileFiltersOpen || selectedSpot || sasquatchPanelOpen
? "opacity-0 pointer-events-none scale-90" ? "opacity-0 pointer-events-none translate-y-2"
: "opacity-100 scale-100" : "opacity-100 translate-y-0"
)} )}
style={{ bottom: 'calc(3.5rem + env(safe-area-inset-bottom, 0px) + 0.75rem)' }} style={{ bottom: 'calc(3.5rem + env(safe-area-inset-bottom, 0px) + 0.75rem)' }}
> >
{/* Add Shop — green circle */} {/* Single "+" FAB — opens a mini menu via state */}
<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 */}
<Link to="/create-spot" aria-label="Add a skate spot"> <Link to="/create-spot" aria-label="Add a skate spot">
<button <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" 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"
aria-label="Add spot"
> >
<Plus className="h-6 w-6 text-white" strokeWidth={3} /> <Plus className="h-5 w-5 text-white" strokeWidth={2.5} />
</button> </button>
</Link> </Link>
</div> </div>
)} )}
{/* Near Me button + attribution — floating top-right, hidden when any overlay is open */} {/* Map attribution — bottom-left corner, minimal */}
<div className={cn( <div className="absolute bottom-2 left-2 z-[999] pointer-events-none">
"absolute top-4 right-4 z-[1000] flex flex-col items-end gap-1.5 transition-opacity duration-150", <div className="bg-black/50 backdrop-blur px-1.5 py-0.5 rounded text-[9px] text-gray-600 leading-snug pointer-events-auto">
mobileFiltersOpen ? "opacity-0 pointer-events-none" : "opacity-100" <a href="https://carto.com/attributions" target="_blank" rel="noopener noreferrer" className="hover:text-gray-400 transition-colors">CARTO</a>
)}>
<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>
{' · '} {' · '}
<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 && ( {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>
</div> </div>
@ -1538,53 +1503,71 @@ export default function MapPage() {
</div> </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 <div
className={cn( className={cn(
"fixed right-3 z-[1000] md:hidden flex flex-col items-center gap-2 transition-opacity duration-150", "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" : "opacity-100" 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)' }} 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: 'spots', emoji: '🛹', active: showSpots, toggle: () => setShowSpots(v => !v) },
{ key: 'shops', label: '🏪', active: showShops, toggle: () => setShowShops(v => !v), activeColor: 'bg-purple-600/90' }, { key: 'shops', emoji: '🏪', active: showShops, toggle: () => setShowShops(v => !v) },
{ key: 'gear', label: '🛒', active: showGear, toggle: () => setShowGear(v => !v), activeColor: 'bg-orange-500/90' }, { key: 'gear', emoji: '🛒', active: showGear, toggle: () => setShowGear(v => !v) },
{ key: 'tours', label: '🗺️', active: showTours, toggle: () => setShowTours(v => !v), activeColor: 'bg-cyan-500/90' }, { key: 'tours', emoji: '🗺️', active: showTours, toggle: () => setShowTours(v => !v) },
].map(({ key, label, active, toggle, activeColor }) => ( ].map(({ key, emoji, active, toggle }) => (
<button <button
key={key} key={key}
onClick={toggle} onClick={toggle}
className={cn( className={cn(
// 40×40 ensures 44pt Apple / 48dp Material touch target compliance 'h-9 w-9 rounded-full text-sm flex items-center justify-center border transition-all touch-manipulation active:scale-90',
'h-10 w-10 rounded-full text-base flex items-center justify-center shadow-md border transition-all touch-manipulation active:scale-90',
active active
? `${activeColor} border-white/20 shadow-black/30` ? 'bg-black/80 border-white/25 shadow-md'
: 'bg-black/60 border-white/8 opacity-40' : 'bg-black/40 border-white/6 opacity-35'
)} )}
title={key}
aria-label={`Toggle ${key}`} aria-label={`Toggle ${key}`}
> >
{label} {emoji}
</button> </button>
))} ))}
{/* Divider */} {/* Divider + Quick Snap */}
{user && <div className="w-4 h-px bg-white/15 my-0.5" />}
{/* Quick Snap */}
{user && ( {user && (
<> <>
<Button <div className="w-4 h-px bg-white/10" />
<button
onClick={() => setShowQuickCamera(true)} 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" /> <Camera className="h-3.5 w-3.5 text-gray-300" />
</Button> </button>
<span className="text-[10px] text-white/60 font-bold whitespace-nowrap">
Snap
</span>
</> </>
)} )}
</div> </div>