Add leaflet-powered brewery map

This commit is contained in:
Aaron Po
2026-08-26 03:40:11 -04:00
parent 4ca35de7ef
commit d6306cbcba
11 changed files with 306 additions and 20 deletions

View File

@@ -1,5 +1,6 @@
import { Link } from 'react-router';
import type { Brewery } from '../breweries.server';
import { formatBreweryAddress } from '../utils/format-address';
interface BreweryCardProps {
brewery: Brewery;
@@ -12,13 +13,32 @@ const BreweryCard = ({ brewery }: BreweryCardProps) => (
>
<div className="card-body">
<h2 className="card-title">{brewery.breweryName}</h2>
<p className="text-base-content/70 line-clamp-3">{brewery.description}</p>
{brewery.location && (
<p className="text-sm font-medium text-base-content/70">
{brewery.location.cityName}, {brewery.location.stateProvinceCode}
</p>
)}
{brewery.description && (
<p className="text-sm leading-5 text-base-content/70 line-clamp-2 max-h-10 overflow-hidden">
{brewery.description}
</p>
)}
{brewery.location && (
<p className="text-sm text-base-content/50 mt-2">
{brewery.location.addressLine1}
{brewery.location.addressLine2 ? `, ${brewery.location.addressLine2}` : ''}
{formatBreweryAddress(brewery.location)}
</p>
)}
{brewery.location?.coordinates && (
<a
href={`https://www.openstreetmap.org/?mlat=${brewery.location.coordinates.latitude}&mlon=${brewery.location.coordinates.longitude}#map=16/${brewery.location.coordinates.latitude}/${brewery.location.coordinates.longitude}`}
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
className="link link-primary text-sm mt-2 self-start"
>
View on map &rarr;
</a>
)}
</div>
</Link>
);

View File

@@ -0,0 +1,78 @@
import { useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L, { type LatLngExpression, type LatLngTuple } from 'leaflet';
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
import 'leaflet/dist/leaflet.css';
import useMediaQuery from '../../../hooks/useMediaQuery';
import type { BreweryLocation } from '../breweries.server';
import { formatBreweryAddress } from '../utils/format-address';
delete (L.Icon.Default.prototype as unknown as { _getIconUrl?: unknown })._getIconUrl;
L.Icon.Default.mergeOptions({ iconRetinaUrl, iconUrl, shadowUrl });
export interface BreweryMapPin {
id: string;
name: string;
latitude: number;
longitude: number;
location: BreweryLocation | null;
}
interface BreweryMapProps {
breweries: BreweryMapPin[];
}
const FALLBACK_CENTER: LatLngExpression = [20, 0];
const FitBounds = ({ bounds }: { bounds: LatLngTuple[] }) => {
const map = useMap();
useEffect(() => {
if (bounds.length > 0) {
map.fitBounds(bounds, { padding: [32, 32] });
}
}, [map, bounds]);
return null;
};
const BreweryMap = ({ breweries }: BreweryMapProps) => {
const bounds: LatLngTuple[] = breweries.map((brewery) => [
brewery.latitude,
brewery.longitude,
]);
return (
<MapContainer
center={bounds[0] ?? FALLBACK_CENTER}
zoom={4}
scrollWheelZoom={false}
className="h-112 md:h-136 w-full rounded-box relative z-0"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<FitBounds bounds={bounds} />
{breweries.map((brewery) => (
<Marker key={brewery.id} position={[brewery.latitude, brewery.longitude]}>
<Popup>
<p className="font-semibold">{brewery.name}</p>
{brewery.location && (
<p className="text-sm text-base-content/70">
{formatBreweryAddress(brewery.location)}
</p>
)}
<a href={`/breweries/${brewery.id}`} className="link link-primary text-sm">
View brewery &rarr;
</a>
</Popup>
</Marker>
))}
</MapContainer>
);
};
export default BreweryMap;