fix(map): speed-dial FAB + location help dialog
FAB (Add Spot / Add Shop): - Removed old bottom-left "Add Spot" pill that sat on top of the bottom nav - New speed-dial FAB in bottom-left, positioned at bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] — always clears the iOS/Android safe area + bottom nav height - Tap the red + button to expand upward: "Add Spot" (red/MapPin) and "Add Shop" (purple/Store) pill buttons slide in with animation - + button rotates 45° when open (becomes ×), closes on map tap - Logged-out state shows "Log in to add" prompt instead - Tapping either option closes the speed-dial before navigating Right-side filter pills: - Bumped from bottom-20 to bottom-[calc(5rem+env(safe-area-inset-bottom))] so they're consistently safe-area aware on devices with home indicator Near Me — location denied fix: - PERMISSION_DENIED no longer shows a toast (which was useless — user can't act on a disappearing toast) - Tapping the red LocateOff button now opens a bottom-sheet help dialog with per-platform instructions (Android Chrome, iOS Safari, desktop Chrome, Firefox) on how to re-enable location permission - "Got it" dismisses and resets the error state so the button returns to normal for a fresh attempt - handleNearMe still fires normally on first tap (not denied state) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
886280e369
commit
2500b549f5
1 changed files with 108 additions and 27 deletions
|
|
@ -68,6 +68,8 @@ import {
|
|||
MapPin,
|
||||
LocateFixed,
|
||||
LocateOff,
|
||||
Store,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { CameraCapture } from '@/components/CameraCapture';
|
||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||
|
|
@ -595,6 +597,10 @@ export default function MapPage() {
|
|||
const [flyTo, setFlyTo] = useState<{ lat: number; lng: number; zoom?: number } | null>(null);
|
||||
const [nearMeLoading, setNearMeLoading] = useState(false);
|
||||
const [nearMeError, setNearMeError] = useState<'denied' | 'unavailable' | 'timeout' | null>(null);
|
||||
const [showLocationHelp, setShowLocationHelp] = useState(false);
|
||||
|
||||
// Speed-dial FAB open state
|
||||
const [fabOpen, setFabOpen] = useState(false);
|
||||
|
||||
const handleNearMe = () => {
|
||||
// Clear previous error
|
||||
|
|
@ -623,11 +629,7 @@ export default function MapPage() {
|
|||
setNearMeLoading(false);
|
||||
if (err.code === err.PERMISSION_DENIED) {
|
||||
setNearMeError('denied');
|
||||
toast({
|
||||
title: 'Location access denied',
|
||||
description: 'To find spots near you, allow location access in your browser settings, then try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
setShowLocationHelp(true);
|
||||
} else if (err.code === err.TIMEOUT) {
|
||||
setNearMeError('timeout');
|
||||
toast({
|
||||
|
|
@ -734,6 +736,8 @@ export default function MapPage() {
|
|||
);
|
||||
|
||||
const handleMapClick = (lat: number, lng: number) => {
|
||||
// Close speed-dial FAB if open
|
||||
setFabOpen(false);
|
||||
if (!user) return;
|
||||
// Navigate to create spot with coordinates pre-filled
|
||||
navigate(`/create-spot?lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}`);
|
||||
|
|
@ -1420,31 +1424,63 @@ export default function MapPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Spot FAB — primary CTA, bottom-left.
|
||||
Hidden when Sasquatch panel is open (user is in chat mode).
|
||||
Hidden when mobile filters or spot sheet is open. */}
|
||||
{/* Speed-dial FAB — bottom-right, above filter pills, clear of bottom nav.
|
||||
Tap the + button to expand into Add Spot + Add Shop options.
|
||||
Hidden when overlays open. Desktop gets a different layout so md:hidden. */}
|
||||
<div className={cn(
|
||||
"absolute bottom-4 left-4 z-[1000] transition-all duration-200 md:hidden",
|
||||
"absolute bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] left-3 z-[1001] md:hidden flex flex-col items-start gap-2 transition-all duration-200",
|
||||
mobileFiltersOpen || selectedSpot || sasquatchPanelOpen
|
||||
? "opacity-0 pointer-events-none scale-90"
|
||||
: "opacity-100 scale-100"
|
||||
)}>
|
||||
{user ? (
|
||||
<Link to="/create-spot">
|
||||
<button className="flex items-center gap-2 h-12 pl-3 pr-4 rounded-full bg-[#ff0040] shadow-xl shadow-[#ff0040]/40 border-2 border-white/20 text-white font-black text-sm uppercase tracking-wide active:scale-95 transition-transform">
|
||||
<Plus className="h-5 w-5 flex-shrink-0" />
|
||||
Add Spot
|
||||
{/* Expanded options — slide up when fabOpen */}
|
||||
<div className={cn(
|
||||
"flex flex-col gap-2 transition-all duration-200 origin-bottom",
|
||||
fabOpen ? "opacity-100 translate-y-0 pointer-events-auto" : "opacity-0 translate-y-3 pointer-events-none"
|
||||
)}>
|
||||
{user ? (
|
||||
<>
|
||||
<Link to="/create-shop" onClick={() => setFabOpen(false)}>
|
||||
<button className="flex items-center gap-2 h-11 pl-3 pr-4 rounded-full bg-purple-600 shadow-lg shadow-purple-600/30 border border-white/20 text-white font-black text-sm uppercase tracking-wide active:scale-95 transition-transform whitespace-nowrap">
|
||||
<Store className="h-4 w-4 flex-shrink-0" />
|
||||
Add Shop
|
||||
</button>
|
||||
</Link>
|
||||
<Link to="/create-spot" onClick={() => setFabOpen(false)}>
|
||||
<button className="flex items-center gap-2 h-11 pl-3 pr-4 rounded-full bg-[#ff0040] shadow-lg shadow-[#ff0040]/30 border border-white/20 text-white font-black text-sm uppercase tracking-wide active:scale-95 transition-transform whitespace-nowrap">
|
||||
<MapPin className="h-4 w-4 flex-shrink-0" />
|
||||
Add Spot
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setFabOpen(false);
|
||||
document.querySelector<HTMLElement>('[data-login-trigger]')?.click();
|
||||
}}
|
||||
className="flex items-center gap-2 h-11 pl-3 pr-4 rounded-full bg-[#ff0040]/80 shadow-lg shadow-[#ff0040]/20 border border-white/20 text-white font-black text-sm uppercase tracking-wide active:scale-95 transition-transform backdrop-blur whitespace-nowrap"
|
||||
>
|
||||
<Plus className="h-4 w-4 flex-shrink-0" />
|
||||
Log in to add
|
||||
</button>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => document.querySelector<HTMLElement>('[data-login-trigger]')?.click()}
|
||||
className="flex items-center gap-2 h-12 pl-3 pr-4 rounded-full bg-[#ff0040]/80 shadow-xl shadow-[#ff0040]/30 border-2 border-white/20 text-white font-black text-sm uppercase tracking-wide active:scale-95 transition-transform backdrop-blur"
|
||||
>
|
||||
<Plus className="h-5 w-5 flex-shrink-0" />
|
||||
Add Spot
|
||||
</button>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main FAB trigger */}
|
||||
<button
|
||||
onClick={() => setFabOpen(v => !v)}
|
||||
aria-label={fabOpen ? 'Close add menu' : 'Add spot or shop'}
|
||||
aria-expanded={fabOpen}
|
||||
className={cn(
|
||||
"h-12 w-12 rounded-full shadow-xl border-2 border-white/20 flex items-center justify-center transition-all duration-200 active:scale-95 touch-manipulation",
|
||||
fabOpen
|
||||
? "bg-white/15 rotate-45 shadow-white/10"
|
||||
: "bg-[#ff0040] shadow-[#ff0040]/40"
|
||||
)}
|
||||
>
|
||||
<Plus className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Near Me button + attribution — floating top-right, hidden when any overlay is open */}
|
||||
|
|
@ -1453,7 +1489,7 @@ export default function MapPage() {
|
|||
mobileFiltersOpen ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
)}>
|
||||
<Button
|
||||
onClick={handleNearMe}
|
||||
onClick={nearMeError === 'denied' ? () => setShowLocationHelp(true) : handleNearMe}
|
||||
disabled={nearMeLoading}
|
||||
size="sm"
|
||||
className={cn(
|
||||
|
|
@ -1463,7 +1499,7 @@ export default function MapPage() {
|
|||
: "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="Find spots near my location"
|
||||
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" />
|
||||
|
|
@ -1485,9 +1521,54 @@ export default function MapPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location permission help dialog */}
|
||||
{showLocationHelp && (
|
||||
<div className="absolute inset-0 z-[2000] flex items-end justify-center pb-[calc(5rem+env(safe-area-inset-bottom,0px))] md:items-center md:pb-0 px-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setShowLocationHelp(false)}
|
||||
/>
|
||||
<div className="relative w-full max-w-sm rounded-2xl bg-[#111] border border-red-500/30 shadow-2xl p-5 space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-red-500/15 flex items-center justify-center flex-shrink-0">
|
||||
<LocateOff className="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-black text-white uppercase tracking-wide">Location Blocked</p>
|
||||
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
|
||||
Your browser has blocked location access for this site. Here's how to re-enable it:
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/8 bg-white/[0.03] divide-y divide-white/5 overflow-hidden">
|
||||
{[
|
||||
{ platform: '📱 Android Chrome', steps: 'Tap the lock icon in the address bar → Site settings → Location → Allow' },
|
||||
{ platform: '🍎 iOS Safari', steps: 'Settings app → Safari → Location → Allow (or tap the "AA" button in Safari\'s address bar)' },
|
||||
{ platform: '💻 Desktop Chrome', steps: 'Click the lock icon → Site settings → Location → Allow' },
|
||||
{ platform: '🦊 Firefox', steps: 'Click the lock icon → Connection secure → More info → Permissions → Access your location → Allow' },
|
||||
].map(({ platform, steps }) => (
|
||||
<div key={platform} className="px-3 py-2.5">
|
||||
<p className="text-xs font-bold text-white mb-0.5">{platform}</p>
|
||||
<p className="text-[11px] text-muted-foreground leading-snug">{steps}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowLocationHelp(false);
|
||||
setNearMeError(null);
|
||||
}}
|
||||
className="w-full py-3 rounded-xl bg-white/8 hover:bg-white/12 border border-white/10 text-sm font-bold text-white transition-all active:scale-[0.98]"
|
||||
>
|
||||
Got it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right-side control column — filter pills + Quick Snap (mobile only, hidden when overlays open) */}
|
||||
<div className={cn(
|
||||
"absolute bottom-20 right-3 z-[1000] md:hidden flex flex-col items-center gap-2 transition-opacity duration-150",
|
||||
"absolute bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] 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"
|
||||
)}>
|
||||
{/* Layer filter pills — compact icon-only */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue