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 = 300;
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;