diff --git a/public/images/user-location.png b/public/images/user-location.png new file mode 100644 index 0000000..a785ca7 Binary files /dev/null and b/public/images/user-location.png differ diff --git a/src/components/SpotMap.tsx b/src/components/SpotMap.tsx index 79c8acb..4584199 100644 --- a/src/components/SpotMap.tsx +++ b/src/components/SpotMap.tsx @@ -106,6 +106,47 @@ const createShopIcon = () => L.divIcon({ popupAnchor: [0, -36], }); +// "You are here" user-location marker — SkateSpots logo in a circular badge +// with a pulsing accuracy halo (Google-Maps-style, brand-flavored). +const createUserLocationIcon = () => { + // BASE_URL resolves correctly for both web ('/') and the Capacitor APK ('./'). + const logoUrl = `${import.meta.env.BASE_URL}images/user-location.png`; + return L.divIcon({ + className: 'user-location-marker', + html: ` +
+ + + +
+
+ `, + iconSize: [64, 64], + iconAnchor: [32, 32], // center anchored (it marks a point, not a pin drop) + popupAnchor: [0, -20], + }); +}; + // Dual-purpose marker (shop + spot) const createDualIcon = (hasGear?: boolean, gearCount?: number) => L.divIcon({ className: 'dual-marker', @@ -391,6 +432,8 @@ interface SpotMapProps { showCrews?: boolean; showBtcMap?: boolean; flyTo?: { lat: number; lng: number; zoom?: number } | null; + /** The current user's location — renders a "you are here" pin when set. */ + userLocation?: { lat: number; lng: number } | null; } function createPopupContent( @@ -637,11 +680,13 @@ export function SpotMap({ showCrews = false, showBtcMap = false, flyTo = null, + userLocation = null, }: SpotMapProps) { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const markersRef = useRef([]); const btcMapMarkersRef = useRef([]); + const userMarkerRef = useRef(null); // MarkerClusterGroup for spot/shop/gear/tour markers // eslint-disable-next-line @typescript-eslint/no-explicit-any const clusterGroupRef = useRef(null); @@ -859,6 +904,36 @@ export function SpotMap({ map.flyTo([flyTo.lat, flyTo.lng], flyTo.zoom ?? 14, { animate: true, duration: 1.2 }); }, [flyTo]); + // Render / update the "you are here" user-location marker. + useEffect(() => { + const map = mapInstanceRef.current; + if (!map) return; + + if (!userLocation) { + // No location — remove any existing marker. + if (userMarkerRef.current) { + userMarkerRef.current.remove(); + userMarkerRef.current = null; + } + return; + } + + if (userMarkerRef.current) { + // Move the existing marker to the new position. + userMarkerRef.current.setLatLng([userLocation.lat, userLocation.lng]); + } else { + // Create it, on top of everything else. + userMarkerRef.current = L.marker([userLocation.lat, userLocation.lng], { + icon: createUserLocationIcon(), + zIndexOffset: 1000, + interactive: true, + keyboard: false, + }) + .addTo(map) + .bindPopup('You are here'); + } + }, [userLocation]); + // Update markers useEffect(() => { const map = mapInstanceRef.current; diff --git a/src/index.css b/src/index.css index 9ad5aee..507e826 100644 --- a/src/index.css +++ b/src/index.css @@ -285,6 +285,13 @@ 50% { box-shadow: 0 0 0 8px hsl(var(--primary) / 0); } } +/* "You are here" user-location marker pulse halo */ +@keyframes ssPulse { + 0% { transform: scale(0.5); opacity: 0.8; } + 70% { transform: scale(1.6); opacity: 0; } + 100% { transform: scale(1.6); opacity: 0; } +} + .animate-fadeIn { animation: fadeIn 0.4s ease-out forwards; } diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index bec10aa..d29c4f4 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -1592,6 +1592,7 @@ export default function MapPage() { showCrews={showCrews} showBtcMap={showBtcMap} flyTo={flyTo} + userLocation={userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null} /> {/* Loading indicator — slim top edge bar */}