mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 10:04:44 +00:00
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { MapContainer, TileLayer, Marker, Popup, CircleMarker, useMap } from 'react-leaflet';
|
|
import L, { type LatLngTuple } from 'leaflet';
|
|
import 'leaflet/dist/leaflet.css';
|
|
|
|
export interface NearbyMapPin {
|
|
id: string;
|
|
name: string;
|
|
cityLabel: string | null;
|
|
latitude: number;
|
|
longitude: number;
|
|
}
|
|
|
|
interface NearbyBreweryMapProps {
|
|
pins: NearbyMapPin[];
|
|
selectedId: string | null;
|
|
onSelect: (id: string) => void;
|
|
userLocation: LatLngTuple | null;
|
|
userLocationLabel: string;
|
|
}
|
|
|
|
const pinIcon = (active: boolean) =>
|
|
L.divIcon({
|
|
className: '',
|
|
html: `<span class="block h-[18px] w-[18px] rounded-full border-[3px] border-base-100 shadow-[0_1px_4px_rgba(0,0,0,.35)] ${
|
|
active ? 'bg-secondary scale-125' : 'bg-primary'
|
|
}"></span>`,
|
|
iconSize: [18, 18],
|
|
iconAnchor: [9, 9],
|
|
});
|
|
|
|
const FitAndFocus = ({
|
|
pins,
|
|
userLocation,
|
|
selectedId,
|
|
}: {
|
|
pins: NearbyMapPin[];
|
|
userLocation: LatLngTuple | null;
|
|
selectedId: string | null;
|
|
}) => {
|
|
const map = useMap();
|
|
const didInitialFit = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (didInitialFit.current) return;
|
|
const points: LatLngTuple[] = pins.map((pin) => [pin.latitude, pin.longitude]);
|
|
if (userLocation) points.push(userLocation);
|
|
if (points.length > 0) {
|
|
map.fitBounds(points, { padding: [40, 40] });
|
|
didInitialFit.current = true;
|
|
}
|
|
setTimeout(() => map.invalidateSize(), 200);
|
|
}, [map, pins, userLocation]);
|
|
|
|
useEffect(() => {
|
|
if (!didInitialFit.current || !selectedId) return;
|
|
const pin = pins.find((p) => p.id === selectedId);
|
|
if (!pin) return;
|
|
map.flyTo([pin.latitude, pin.longitude], Math.max(map.getZoom(), 11), { duration: 0.8 });
|
|
}, [map, pins, selectedId]);
|
|
|
|
return null;
|
|
};
|
|
|
|
const NearbyBreweryMap = ({
|
|
pins,
|
|
selectedId,
|
|
onSelect,
|
|
userLocation,
|
|
userLocationLabel,
|
|
}: NearbyBreweryMapProps) => (
|
|
<MapContainer
|
|
center={userLocation ?? [pins[0]?.latitude ?? 20, pins[0]?.longitude ?? 0]}
|
|
zoom={11}
|
|
scrollWheelZoom={false}
|
|
className="min-h-[30rem] h-full w-full rounded-box relative z-0"
|
|
>
|
|
<TileLayer
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
/>
|
|
<FitAndFocus pins={pins} userLocation={userLocation} selectedId={selectedId} />
|
|
{userLocation && (
|
|
<CircleMarker
|
|
center={userLocation}
|
|
radius={6}
|
|
pathOptions={{ color: '#fff', weight: 2, fillColor: '#2b6cb0', fillOpacity: 1 }}
|
|
>
|
|
<Popup>You are here — {userLocationLabel}</Popup>
|
|
</CircleMarker>
|
|
)}
|
|
{pins.map((pin) => (
|
|
<Marker
|
|
key={pin.id}
|
|
position={[pin.latitude, pin.longitude]}
|
|
icon={pinIcon(pin.id === selectedId)}
|
|
eventHandlers={{ click: () => onSelect(pin.id) }}
|
|
>
|
|
<Popup>
|
|
<p className="font-semibold font-serif">{pin.name}</p>
|
|
{pin.cityLabel && <p className="text-sm text-base-content/70">{pin.cityLabel}</p>}
|
|
<a href={`/breweries/${pin.id}`} className="link link-primary text-sm">
|
|
View brewery →
|
|
</a>
|
|
</Popup>
|
|
</Marker>
|
|
))}
|
|
</MapContainer>
|
|
);
|
|
|
|
export default NearbyBreweryMap;
|