Map: add "you are here" user-location marker (SkateSpots logo + pulse)
After Near Me centers the map, there was no indicator of where the user
actually is. Add a Google-Maps-style location marker, brand-flavored:
- Circular badge using the SkateSpots logo (public/images/user-location.png)
with a red (#ff0040) ring, sitting at the user's exact coordinates.
- Pulsing translucent accuracy halo (ssPulse keyframes) so it reads as a
live "you are here" and doubles as subtle motion.
- Rendered above all other markers (zIndexOffset), with a "You are here"
popup. Created/moved/removed reactively from a new `userLocation` prop.
Wired MapPage's userLocation state through to SpotMap (lat/lon → lat/lng).
Marker image path uses import.meta.env.BASE_URL so it resolves on both
web ('/') and the Capacitor APK ('./').
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
59a801d525
commit
58aaeb7b16
4 changed files with 83 additions and 0 deletions
BIN
public/images/user-location.png
Normal file
BIN
public/images/user-location.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
|
|
@ -106,6 +106,47 @@ const createShopIcon = () => L.divIcon({
|
||||||
popupAnchor: [0, -36],
|
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: `
|
||||||
|
<div style="position: relative; width: 64px; height: 64px;">
|
||||||
|
<!-- pulsing accuracy halo -->
|
||||||
|
<span style="
|
||||||
|
position: absolute;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
width: 64px; height: 64px;
|
||||||
|
margin: -32px 0 0 -32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,0,64,0.25);
|
||||||
|
animation: ssPulse 2s ease-out infinite;
|
||||||
|
"></span>
|
||||||
|
<!-- logo badge -->
|
||||||
|
<div style="
|
||||||
|
position: absolute;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
margin: -20px 0 0 -20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-image: url('${logoUrl}');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 3px solid #ff0040;
|
||||||
|
box-shadow: 0 3px 10px rgba(0,0,0,0.5);
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
iconSize: [64, 64],
|
||||||
|
iconAnchor: [32, 32], // center anchored (it marks a point, not a pin drop)
|
||||||
|
popupAnchor: [0, -20],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Dual-purpose marker (shop + spot)
|
// Dual-purpose marker (shop + spot)
|
||||||
const createDualIcon = (hasGear?: boolean, gearCount?: number) => L.divIcon({
|
const createDualIcon = (hasGear?: boolean, gearCount?: number) => L.divIcon({
|
||||||
className: 'dual-marker',
|
className: 'dual-marker',
|
||||||
|
|
@ -391,6 +432,8 @@ interface SpotMapProps {
|
||||||
showCrews?: boolean;
|
showCrews?: boolean;
|
||||||
showBtcMap?: boolean;
|
showBtcMap?: boolean;
|
||||||
flyTo?: { lat: number; lng: number; zoom?: number } | null;
|
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(
|
function createPopupContent(
|
||||||
|
|
@ -637,11 +680,13 @@ export function SpotMap({
|
||||||
showCrews = false,
|
showCrews = false,
|
||||||
showBtcMap = false,
|
showBtcMap = false,
|
||||||
flyTo = null,
|
flyTo = null,
|
||||||
|
userLocation = null,
|
||||||
}: SpotMapProps) {
|
}: SpotMapProps) {
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
const mapInstanceRef = useRef<L.Map | null>(null);
|
const mapInstanceRef = useRef<L.Map | null>(null);
|
||||||
const markersRef = useRef<L.Marker[]>([]);
|
const markersRef = useRef<L.Marker[]>([]);
|
||||||
const btcMapMarkersRef = useRef<L.Marker[]>([]);
|
const btcMapMarkersRef = useRef<L.Marker[]>([]);
|
||||||
|
const userMarkerRef = useRef<L.Marker | null>(null);
|
||||||
// MarkerClusterGroup for spot/shop/gear/tour markers
|
// MarkerClusterGroup for spot/shop/gear/tour markers
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const clusterGroupRef = useRef<any>(null);
|
const clusterGroupRef = useRef<any>(null);
|
||||||
|
|
@ -859,6 +904,36 @@ export function SpotMap({
|
||||||
map.flyTo([flyTo.lat, flyTo.lng], flyTo.zoom ?? 14, { animate: true, duration: 1.2 });
|
map.flyTo([flyTo.lat, flyTo.lng], flyTo.zoom ?? 14, { animate: true, duration: 1.2 });
|
||||||
}, [flyTo]);
|
}, [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('<b>You are here</b>');
|
||||||
|
}
|
||||||
|
}, [userLocation]);
|
||||||
|
|
||||||
// Update markers
|
// Update markers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapInstanceRef.current;
|
const map = mapInstanceRef.current;
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,13 @@
|
||||||
50% { box-shadow: 0 0 0 8px hsl(var(--primary) / 0); }
|
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 {
|
.animate-fadeIn {
|
||||||
animation: fadeIn 0.4s ease-out forwards;
|
animation: fadeIn 0.4s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1592,6 +1592,7 @@ export default function MapPage() {
|
||||||
showCrews={showCrews}
|
showCrews={showCrews}
|
||||||
showBtcMap={showBtcMap}
|
showBtcMap={showBtcMap}
|
||||||
flyTo={flyTo}
|
flyTo={flyTo}
|
||||||
|
userLocation={userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{/* Loading indicator — slim top edge bar */}
|
{/* Loading indicator — slim top edge bar */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue