diff --git a/src/components/SpotMap.tsx b/src/components/SpotMap.tsx index c72bc86..eccaadf 100644 --- a/src/components/SpotMap.tsx +++ b/src/components/SpotMap.tsx @@ -321,6 +321,7 @@ interface SpotMapProps { showGear?: boolean; showTours?: boolean; showBtcMap?: boolean; + flyTo?: { lat: number; lng: number; zoom?: number } | null; } function createPopupContent( @@ -562,6 +563,7 @@ export function SpotMap({ showGear = true, showTours = true, showBtcMap = false, + flyTo = null, }: SpotMapProps) { const mapRef = useRef(null); const mapInstanceRef = useRef(null); @@ -670,9 +672,9 @@ export function SpotMap({ maxBoundsViscosity: 1.0, }); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '', - // Match minZoom so tiles are always available at the minimum zoom level + subdomains: 'abcd', minZoom: 2, }).addTo(map); @@ -777,6 +779,13 @@ export function SpotMap({ } }, [selectedSpot]); + // Fly to location when flyTo prop changes + useEffect(() => { + const map = mapInstanceRef.current; + if (!map || !flyTo) return; + map.flyTo([flyTo.lat, flyTo.lng], flyTo.zoom ?? 14, { animate: true, duration: 1.2 }); + }, [flyTo]); + // Update markers useEffect(() => { const map = mapInstanceRef.current; @@ -1133,15 +1142,23 @@ export function SpotMap({ {/* OpenStreetMap Attribution */}
Map data Β©{' '} + + CARTO + + {' '}Β·{' '} - OpenStreetMap + OSM - {' '}contributors {showBtcMap && ( <> {' β€’ '} diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index b87bb6a..46be49a 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -12,7 +12,7 @@ import { SortDropdown, type SortOption, calculateDistance } from '@/components/S import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from '@/components/ui/sheet'; import { Card, CardContent } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Progress } from '@/components/ui/progress'; @@ -62,6 +62,8 @@ import { SlidersHorizontal, Check, Camera, + Navigation, + ExternalLink, } from 'lucide-react'; import { CameraCapture } from '@/components/CameraCapture'; import { useUploadFile } from '@/hooks/useUploadFile'; @@ -323,6 +325,111 @@ function StashDrawer() { ); } +// Bottom sheet shown when a map marker is tapped +function SpotBottomSheet({ spot, onClose }: { spot: SkateSpot; onClose: () => void }) { + const navigate = useNavigate(); + const author = useAuthor(spot.pubkey); + const metadata = author.data?.metadata; + const displayName = metadata?.name ?? genUserName(spot.pubkey); + const isShop = spot.types.includes('shop'); + const route = isShop ? `/shop/${spot.pubkey}/${spot.identifier}` : `/spot/${spot.pubkey}/${spot.identifier}`; + + return ( +
+ {/* Drag handle */} +
+
+
+ +
+ {/* Header row */} +
+ {/* Thumbnail */} +
+ {spot.images[0] ? ( + {spot.title} + ) : ( +
+ {isShop ? 'πŸͺ' : 'πŸ›Ή'} +
+ )} +
+ + {/* Info */} +
+
+

{spot.title}

+
+ {spot.location && ( +

+ + {spot.location} +

+ )} + {/* Type badges */} +
+ {spot.types.slice(0, 3).map((type) => ( + + {SPOT_TYPE_LABELS[type] || type} + + ))} +
+
+ + {/* Close */} + +
+ + {/* Description */} + {spot.description && ( +

{spot.description}

+ )} + + {/* Author */} +
+ + + {displayName.slice(0, 2).toUpperCase()} + + Added by {displayName} +
+ + {/* CTA buttons */} +
+ + {spot.geohash && (() => { + const { latitude, longitude } = ngeohash.decode(spot.geohash); + return ( + + ); + })()} +
+
+
+ ); +} + export default function MapPage() { const { t } = useTranslation(); @@ -377,6 +484,24 @@ export default function MapPage() { // BTC Map center for fetching places const [mapCenter, setMapCenter] = useState<{ lat: number; lon: number }>({ lat: 40.7128, lon: -74.006 }); + + // Near Me fly-to state + const [flyTo, setFlyTo] = useState<{ lat: number; lng: number; zoom?: number } | null>(null); + const [nearMeLoading, setNearMeLoading] = useState(false); + + const handleNearMe = () => { + if (!navigator.geolocation) return; + setNearMeLoading(true); + navigator.geolocation.getCurrentPosition( + (pos) => { + setFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 13 }); + setUserLocation({ lat: pos.coords.latitude, lon: pos.coords.longitude }); + setNearMeLoading(false); + }, + () => setNearMeLoading(false), + { enableHighAccuracy: true, timeout: 8000 } + ); + }; const categoryKeys = Object.keys(SPOT_CATEGORIES); @@ -1111,6 +1236,7 @@ export default function MapPage() { showGear={showGear} showTours={showTours} showBtcMap={showBtcMap} + flyTo={flyTo} /> {/* Loading indicator */} @@ -1123,6 +1249,47 @@ export default function MapPage() {
)} + {/* Floating layer filter pills β€” bottom-left above attribution */} +
+ {[ + { key: 'spots', label: 'πŸ›Ή', active: showSpots, toggle: () => setShowSpots(v => !v), color: 'bg-red-500' }, + { key: 'shops', label: 'πŸͺ', active: showShops, toggle: () => setShowShops(v => !v), color: 'bg-purple-600' }, + { key: 'gear', label: 'πŸ›’', active: showGear, toggle: () => setShowGear(v => !v), color: 'bg-orange-500' }, + { key: 'tours', label: 'πŸ—ΊοΈ', active: showTours, toggle: () => setShowTours(v => !v), color: 'bg-cyan-500' }, + ].map(({ key, label, active, toggle, color }) => ( + + ))} +
+ + {/* Near Me button β€” floating top-right */} +
+ +
+ {/* Quick Capture FAB - only show for logged in users on mobile */} {user && (
@@ -1198,6 +1365,16 @@ export default function MapPage() { onCapture={handleQuickCameraCapture} isUploading={isUploadingQuickPhoto} /> + + {/* Spot Bottom Sheet β€” slides up when a marker is tapped, map stays visible */} + { if (!open) setSelectedSpot(null); }}> + + {selectedSpot && setSelectedSpot(null)} />} + + ); }