import { lazy, Suspense, useEffect, useState } from 'react'; import { useFetcher } from 'react-router'; import type { LatLngTuple } from 'leaflet'; import ClientOnly from '../../../components/ClientOnly'; import useDebouncedValue from '../../../hooks/useDebouncedValue'; import type { SimplifiedBrewery } from '../breweries.server'; import type { loader as nearbyLoader } from '../routes/breweries-nearby'; import { formatDistance, type DistanceUnit } from '../utils/distance'; import type { NearbyMapPin } from './NearbyBreweryMap'; const NearbyBreweryMap = lazy(() => import('./NearbyBreweryMap')); const MAP_PLACEHOLDER_CLASS = 'skeleton min-h-[30rem] h-full w-full rounded-box'; const DEFAULT_RADIUS_KM = 120; const MIN_RADIUS_KM = 10; const MAX_RADIUS_KM = 150; const RADIUS_STEP_KM = 10; const RADIUS_DEBOUNCE_MS = 300; export interface FeaturedPin { id: string; name: string; latitude: number; longitude: number; } interface FallbackCenter { latitude: number; longitude: number; label: string; } interface NearbyBreweriesSectionProps { featured: FeaturedPin | null; fallbackCenter: FallbackCenter | null; selectedId: string | null; onSelect: (id: string) => void; } // Notes aren't tracked on brewery locations yet - see BREWERY_INDEX_HANDOFF.md. const FILLER_NOTE = 'A stop worth the detour.'; const cardClassName = (selected: boolean) => `card card-sm card-border bg-base-100 text-left shadow transition-shadow hover:shadow-lg ${ selected ? 'border-2 border-primary' : '' }`; interface BreweriesNearByHeaderParams { center: LatLngTuple | null; withDistance: { brewery: SimplifiedBrewery; distanceMetres: number }[]; radiusKm: number; unit: 'km' | 'mi'; centerLabel: string; } const BreweriesNearByHeader = ({ center, centerLabel, radiusKm, unit, withDistance, }: BreweriesNearByHeaderParams) => { return (

Breweries near you

{center ? `${withDistance.length} within ${formatDistance(radiusKm * 1000, unit)} of ${centerLabel}` : 'Finding breweries near you…'}
); }; interface RadiusUnitControlsProps { radiusKm: number; unit: DistanceUnit; onRadiusChange: (event: React.ChangeEvent) => void; onSelectKm: () => void; onSelectMi: () => void; } const RadiusUnitControls = ({ radiusKm, unit, onRadiusChange, onSelectKm, onSelectMi, }: RadiusUnitControlsProps) => { return (
); }; interface BreweryWithDistance { brewery: SimplifiedBrewery; distanceMetres: number; } interface NearbyBreweryListProps { withDistance: BreweryWithDistance[]; center: LatLngTuple | null; radiusKm: number; unit: DistanceUnit; renderItem: (item: BreweryWithDistance) => React.JSX.Element; } const NearbyBreweryList = ({ withDistance, center, radiusKm, unit, renderItem, }: NearbyBreweryListProps) => { return ( <> {withDistance.length === 0 && center && (

No partner breweries within {formatDistance(radiusKm * 1000, unit)}.

)} {withDistance.map(renderItem)} ); }; interface ChangeLocationButtonProps { onClick: () => void; } const ChangeLocationButton = ({ onClick }: ChangeLocationButtonProps) => { return (
); }; interface ClientOnlyMapProps { render: () => React.JSX.Element; } const ClientOnlyMap = ({ render }: ClientOnlyMapProps) => { return }>{render}; }; const NearbyBreweriesSection = ({ featured, fallbackCenter, selectedId, onSelect, }: NearbyBreweriesSectionProps) => { const [userLocation, setUserLocation] = useState(null); const [locationLabel, setLocationLabel] = useState('your location'); const [geoAttempted, setGeoAttempted] = useState(false); const [radiusKm, setRadiusKm] = useState(DEFAULT_RADIUS_KM); const debouncedRadiusKm = useDebouncedValue(radiusKm, RADIUS_DEBOUNCE_MS); const [unit, setUnit] = useState('km'); const fetcher = useFetcher(); const requestGeolocation = () => { if (!navigator.geolocation) { Promise.resolve().then(() => setGeoAttempted(true)); return; } navigator.geolocation.getCurrentPosition( (pos) => { setUserLocation([pos.coords.latitude, pos.coords.longitude]); setLocationLabel('your location'); setGeoAttempted(true); }, () => setGeoAttempted(true), { enableHighAccuracy: true }, ); }; useEffect(() => { requestGeolocation(); }, []); const center: LatLngTuple | null = userLocation ?? (geoAttempted && fallbackCenter ? [fallbackCenter.latitude, fallbackCenter.longitude] : null); const centerLabel = userLocation ? locationLabel : (fallbackCenter?.label ?? locationLabel); useEffect(() => { if (!center) return; fetcher.load( `/breweries/nearby?latitude=${center[0]}&longitude=${center[1]}&rangeInMetres=${debouncedRadiusKm * 1000}`, ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [center?.[0], center?.[1], debouncedRadiusKm]); const nearby: SimplifiedBrewery[] = fetcher.data?.breweries ?? []; const withDistance = center ? nearby .filter((b) => b.location?.coordinates && b.distanceMetres !== null) .map((b) => ({ brewery: b, distanceMetres: b.distanceMetres as number, })) .sort((a, b) => a.distanceMetres - b.distanceMetres) : []; const pins: NearbyMapPin[] = withDistance.map(({ brewery }) => ({ id: brewery.breweryPostId, name: brewery.breweryName, cityLabel: brewery.location ? `${brewery.location.cityName}, ${brewery.location.stateProvinceCode}` : null, latitude: brewery.location!.coordinates!.latitude, longitude: brewery.location!.coordinates!.longitude, })); if (featured && !pins.some((p) => p.id === featured.id)) { pins.push({ id: featured.id, name: featured.name, cityLabel: null, latitude: featured.latitude, longitude: featured.longitude, }); } return (
setRadiusKm(Number(e.target.value))} onSelectKm={() => setUnit('km')} onSelectMi={() => setUnit('mi')} /> ( )} />
center ? ( }> ) : (
) } />
); }; export default NearbyBreweriesSection;