On xs + viewMode=map, render a map-first layout: full-bleed map, floating card-shaped control strip at the top (search + Filters + Sort + compact List/Map toggle), and a bottom drawer that slides up when a pin or cluster is tapped. The desktop list-map layout is unchanged. On xs + viewMode=list, the List/Map toggle now appears in the sticky control bar (icon-only) so users can reach the map from the list view. On desktop the toggle stays on the map panel as before. Drawer content: - Single pin → the existing ProviderCard molecule, entire card clickable (navigates to packages) - Cluster → a list of image-free rows (verified icon slot + name + location + rating), tap a row to pan+zoom into the provider - Close X on the drawer clears the active state To support externalising popups, ProviderMap gains two opt-in props (`externalisePopups`, `onActiveChange`) and an imperative handle (`clearActive`, `drillIntoProvider`). Desktop behaviour unchanged when these aren't used. The forwardRef now exposes the handle rather than the DOM element; no existing callsite passed a DOM ref. The filter-dialog children are now defined once as a shared JSX fragment used by both desktop and mobile FilterPanel instances. Header + subhead are suppressed on the mobile map view (per concept reference); they remain on desktop and mobile list for orientation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
550 lines
21 KiB
TypeScript
550 lines
21 KiB
TypeScript
import React from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import Box from '@mui/material/Box';
|
|
import type { SxProps, Theme } from '@mui/material/styles';
|
|
import {
|
|
APIProvider,
|
|
Map as GoogleMap,
|
|
AdvancedMarker,
|
|
useMap,
|
|
useMapsLibrary,
|
|
} from '@vis.gl/react-google-maps';
|
|
import { MarkerClusterer, GridAlgorithm } from '@googlemaps/markerclusterer';
|
|
import { MapPin } from '../../atoms/MapPin';
|
|
import { ClusterMarker } from '../../atoms/ClusterMarker';
|
|
import { MapPopup } from '../../molecules/MapPopup';
|
|
import { ClusterPopup } from '../../molecules/ClusterPopup';
|
|
import { Typography } from '../../atoms/Typography';
|
|
import type { ProviderData } from '../../pages/ProvidersStep';
|
|
|
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
|
|
/** Sydney — fallback centre when no providers have coords and no default supplied */
|
|
const FALLBACK_CENTER = { lat: -33.8688, lng: 151.2093 };
|
|
const FALLBACK_ZOOM = 5;
|
|
/** Google Maps requires a mapId for AdvancedMarker support */
|
|
const MAP_ID = 'fa-provider-map';
|
|
/** fitBounds padding (applied as google.maps.Padding) */
|
|
const BOUNDS_PADDING = { top: 64, right: 48, bottom: 64, left: 48 };
|
|
/** Screen-pixel radius at which nearby pins collapse into a cluster */
|
|
const CLUSTER_GRID_SIZE = 70;
|
|
/** Zoom level above which clustering is disabled (pins show individually) */
|
|
const CLUSTER_MAX_ZOOM = 13;
|
|
/** Zoom level the map animates to on cluster drill-in (street-level, past
|
|
* CLUSTER_MAX_ZOOM so nearby cluster members break apart into their own pins) */
|
|
const DRILL_IN_ZOOM = 15;
|
|
/** Exit-animation duration for popups on close — keep in sync with the
|
|
* transition values set on MapPopup/ClusterPopup. */
|
|
const POPUP_EXIT_MS = 180;
|
|
|
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
/** Shape of the currently-active provider or cluster selection, emitted to
|
|
* callers that opt into external popup rendering (see `externalisePopups`). */
|
|
export interface ProviderMapActiveState {
|
|
/** Active single provider, if a pin was tapped (or a cluster row drilled into) */
|
|
provider: ProviderData | null;
|
|
/** Active cluster, if a cluster marker was tapped and no row has been drilled into */
|
|
cluster: { providers: ProviderData[]; position: { lat: number; lng: number } } | null;
|
|
/** True while the exit animation is running — callers may want to mirror it */
|
|
exiting: boolean;
|
|
}
|
|
|
|
/** Imperative handle exposed via ref. Used when rendering popups externally. */
|
|
export interface ProviderMapHandle {
|
|
/** Close the currently-active popup (animated). No-op if nothing is open. */
|
|
clearActive: () => void;
|
|
/** Pan + zoom the map to a provider's coords and set them as the active
|
|
* single-provider selection. Equivalent to a cluster-row tap. */
|
|
drillIntoProvider: (id: string) => void;
|
|
}
|
|
|
|
/** Props for the FA ProviderMap organism */
|
|
export interface ProviderMapProps {
|
|
/** Providers to render as pins. Providers without coords are filtered out silently. */
|
|
providers: ProviderData[];
|
|
/** ID of the provider whose popup should open (external selection, e.g. list hover) */
|
|
selectedProviderId?: string | null;
|
|
/** Called when the user clicks through a popup — usually triggers navigation */
|
|
onSelectProvider: (id: string) => void;
|
|
/** Initial map centre — used only when no providers have coords */
|
|
defaultCenter?: { lat: number; lng: number };
|
|
/** Initial zoom — used only when no providers have coords */
|
|
defaultZoom?: number;
|
|
/** Google Maps API key. Defaults to `import.meta.env.VITE_GOOGLE_MAPS_API_KEY`. */
|
|
apiKey?: string;
|
|
/** When true, suppress the organism's own MapPopup + ClusterPopup rendering.
|
|
* The active state is still tracked internally (pins still hide when active)
|
|
* and emitted via `onActiveChange` so callers can render a drawer, sheet,
|
|
* or other external container. Used by the mobile map-first layout. */
|
|
externalisePopups?: boolean;
|
|
/** Fires whenever the active provider/cluster state changes. Paired with
|
|
* `externalisePopups` — the caller uses this to drive external UI. */
|
|
onActiveChange?: (state: ProviderMapActiveState) => void;
|
|
/** MUI sx prop for the root element */
|
|
sx?: SxProps<Theme>;
|
|
}
|
|
|
|
interface ActiveCluster {
|
|
providers: ProviderData[];
|
|
position: google.maps.LatLngLiteral;
|
|
}
|
|
|
|
// ─── Internal components ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Fits the map to the bounds of all providers with coords. Runs whenever the
|
|
* provider list changes. Sited inside APIProvider so `useMap()` resolves.
|
|
*/
|
|
const FitBounds: React.FC<{ providers: ProviderData[] }> = ({ providers }) => {
|
|
const map = useMap();
|
|
React.useEffect(() => {
|
|
if (!map) return;
|
|
const withCoords = providers.filter((p) => p.coords);
|
|
if (withCoords.length === 0) return;
|
|
if (withCoords.length === 1) {
|
|
map.setCenter(withCoords[0].coords!);
|
|
map.setZoom(13);
|
|
return;
|
|
}
|
|
const bounds = new window.google.maps.LatLngBounds();
|
|
withCoords.forEach((p) => bounds.extend(p.coords!));
|
|
map.fitBounds(bounds, BOUNDS_PADDING);
|
|
}, [map, providers]);
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Captures the Google Map instance into a parent ref so imperative
|
|
* actions (panTo, setZoom) can be triggered from outside the Map context.
|
|
*/
|
|
const MapRefCapture: React.FC<{
|
|
mapRef: React.MutableRefObject<google.maps.Map | null>;
|
|
}> = ({ mapRef }) => {
|
|
const map = useMap();
|
|
React.useEffect(() => {
|
|
mapRef.current = map;
|
|
}, [map, mapRef]);
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Imperative marker layer — builds AdvancedMarker instances with React
|
|
* content, groups them via MarkerClusterer, and rebuilds whenever the
|
|
* visible provider set changes.
|
|
*
|
|
* Providers listed in `hiddenIds` are excluded from the map (their popup is
|
|
* currently showing instead).
|
|
*/
|
|
const MarkerLayer: React.FC<{
|
|
providers: ProviderData[];
|
|
hiddenIds: Set<string>;
|
|
onPinClick: (id: string) => void;
|
|
onClusterClick: (providers: ProviderData[], position: google.maps.LatLngLiteral) => void;
|
|
}> = ({ providers, hiddenIds, onPinClick, onClusterClick }) => {
|
|
const map = useMap();
|
|
const markerLibrary = useMapsLibrary('marker');
|
|
|
|
// Stash callbacks in a ref so the effect below doesn't re-run (and rebuild
|
|
// every marker) when the parent passes fresh arrow-function references.
|
|
const onPinClickRef = React.useRef(onPinClick);
|
|
const onClusterClickRef = React.useRef(onClusterClick);
|
|
React.useEffect(() => {
|
|
onPinClickRef.current = onPinClick;
|
|
onClusterClickRef.current = onClusterClick;
|
|
}, [onPinClick, onClusterClick]);
|
|
|
|
React.useEffect(() => {
|
|
if (!map || !markerLibrary) return;
|
|
|
|
const roots: Root[] = [];
|
|
const markerToProvider = new Map<google.maps.marker.AdvancedMarkerElement, ProviderData>();
|
|
|
|
const markers = providers
|
|
.filter((p) => p.coords && !hiddenIds.has(p.id))
|
|
.map((p) => {
|
|
const el = document.createElement('div');
|
|
const root = createRoot(el);
|
|
// MapPin's own onClick stays for keyboard a11y (Enter/Space via its
|
|
// onKeyDown). stopPropagation guards against the DOM click bubbling
|
|
// to the Map's onClick and closing the popup the same frame it opens.
|
|
root.render(
|
|
<MapPin
|
|
name={p.name}
|
|
price={p.startingPrice}
|
|
verified={p.verified}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onPinClickRef.current(p.id);
|
|
}}
|
|
/>,
|
|
);
|
|
roots.push(root);
|
|
|
|
const marker = new markerLibrary.AdvancedMarkerElement({
|
|
position: p.coords,
|
|
content: el,
|
|
gmpClickable: true,
|
|
});
|
|
// Also listen at the Google Maps level + stop the GMaps event so
|
|
// Map's onClick can't fire when a pin is clicked via mouse. Safe to
|
|
// fire twice with keyboard — handlePinClick is idempotent.
|
|
marker.addListener('click', (event: google.maps.MapMouseEvent) => {
|
|
event.stop();
|
|
onPinClickRef.current(p.id);
|
|
});
|
|
markerToProvider.set(marker, p);
|
|
return marker;
|
|
});
|
|
|
|
const clusterer = new MarkerClusterer({
|
|
map,
|
|
markers,
|
|
algorithm: new GridAlgorithm({
|
|
maxZoom: CLUSTER_MAX_ZOOM,
|
|
gridSize: CLUSTER_GRID_SIZE,
|
|
}),
|
|
// Override the library's default "zoom to fit cluster" on click —
|
|
// we open the cluster popup instead. The event shape the library
|
|
// passes varies: sometimes a google.maps.MapMouseEvent (has .stop),
|
|
// sometimes a plain DOM MouseEvent. Stop whichever we got so the
|
|
// click doesn't also fire Map.onClick and clear our state.
|
|
onClusterClick: (event, cluster) => {
|
|
const anyEvent = event as unknown as {
|
|
stop?: () => void;
|
|
stopPropagation?: () => void;
|
|
domEvent?: { stopPropagation?: () => void };
|
|
};
|
|
anyEvent.stop?.();
|
|
anyEvent.stopPropagation?.();
|
|
anyEvent.domEvent?.stopPropagation?.();
|
|
|
|
const providersInCluster = cluster.markers
|
|
.map((m) => markerToProvider.get(m as google.maps.marker.AdvancedMarkerElement))
|
|
.filter((p): p is ProviderData => !!p);
|
|
const clusterPosition =
|
|
cluster.position instanceof window.google.maps.LatLng
|
|
? cluster.position.toJSON()
|
|
: (cluster.position as google.maps.LatLngLiteral);
|
|
onClusterClickRef.current(providersInCluster, clusterPosition);
|
|
},
|
|
renderer: {
|
|
render: ({ count, position, markers: clusterMarkers }) => {
|
|
const providersInCluster = clusterMarkers
|
|
.map((m) => markerToProvider.get(m as google.maps.marker.AdvancedMarkerElement))
|
|
.filter((p): p is ProviderData => !!p);
|
|
const hasVerified = providersInCluster.some((p) => p.verified);
|
|
|
|
const el = document.createElement('div');
|
|
const root = createRoot(el);
|
|
// Visual only — click is handled at the MarkerClusterer level above.
|
|
root.render(<ClusterMarker count={count} hasVerified={hasVerified} />);
|
|
roots.push(root);
|
|
|
|
return new markerLibrary.AdvancedMarkerElement({
|
|
position,
|
|
content: el,
|
|
gmpClickable: true,
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
return () => {
|
|
clusterer.clearMarkers();
|
|
// Defer unmount so React doesn't warn about unmounting during render.
|
|
setTimeout(() => {
|
|
roots.forEach((r) => r.unmount());
|
|
}, 0);
|
|
};
|
|
}, [map, markerLibrary, providers, hiddenIds]);
|
|
|
|
return null;
|
|
};
|
|
|
|
/** Empty-state shown when no API key is configured or no providers have coords. */
|
|
const MapEmptyState: React.FC<{ reason: 'no-key' | 'no-coords' }> = ({ reason }) => (
|
|
<Box sx={{ m: 'auto', textAlign: 'center', px: 3 }}>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 0.5 }}>
|
|
Map unavailable
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{reason === 'no-key'
|
|
? 'Google Maps API key not configured.'
|
|
: 'No provider locations to display.'}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
|
|
// ─── Component ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Google Map showing provider pins with clustering + click-to-open popups.
|
|
*
|
|
* **Interaction model:**
|
|
* - Clicking an individual pin **morphs** it into a `MapPopup` at the same
|
|
* coord. Clicking the map background reverts.
|
|
* - Pins within `CLUSTER_GRID_SIZE` (70px) of each other collapse into a
|
|
* `ClusterMarker` — but only while zoomed out at level `CLUSTER_MAX_ZOOM`
|
|
* (13) or below. Zoom in past that and every pin shows individually.
|
|
* - Clicking a cluster opens a `ClusterPopup` listing its providers
|
|
* (verified-first). Clicking a row **pans and zooms the map to that
|
|
* provider's location** (zoom 15 = past the clustering ceiling, so the
|
|
* other cluster members separate into their own pins around the selected
|
|
* one) and opens that provider's `MapPopup`. The cluster state is cleared
|
|
* — there's no back-to-list; the user's path forward is clear rather than
|
|
* hierarchical.
|
|
*
|
|
* **Viewport:** auto-fits to include every provider with coords on load and
|
|
* when the list changes. Single-provider maps centre with zoom 13.
|
|
*
|
|
* **Empty states:** if no API key is set or no providers have coords, a
|
|
* subtle empty state renders in place (no throw).
|
|
*
|
|
* Composes `MapPin` + `ClusterMarker` (atoms) + `MapPopup` + `ClusterPopup`
|
|
* (molecules). Clustering via `@googlemaps/markerclusterer`.
|
|
*/
|
|
export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>(
|
|
(
|
|
{
|
|
providers,
|
|
selectedProviderId,
|
|
onSelectProvider,
|
|
defaultCenter = FALLBACK_CENTER,
|
|
defaultZoom = FALLBACK_ZOOM,
|
|
apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
|
|
externalisePopups = false,
|
|
onActiveChange,
|
|
sx,
|
|
},
|
|
ref,
|
|
) => {
|
|
const [activeProviderId, setActiveProviderId] = React.useState<string | null>(null);
|
|
const [activeCluster, setActiveCluster] = React.useState<ActiveCluster | null>(null);
|
|
const [exiting, setExiting] = React.useState(false);
|
|
const mapRef = React.useRef<google.maps.Map | null>(null);
|
|
const exitTimerRef = React.useRef<number | null>(null);
|
|
|
|
// Helper: cancel any pending exit timer so rapid clicks don't clobber
|
|
// newly-opened popups with a leftover clear from a previous close.
|
|
const cancelExit = React.useCallback(() => {
|
|
if (exitTimerRef.current) {
|
|
window.clearTimeout(exitTimerRef.current);
|
|
exitTimerRef.current = null;
|
|
}
|
|
setExiting(false);
|
|
}, []);
|
|
|
|
React.useEffect(
|
|
() => () => {
|
|
if (exitTimerRef.current) window.clearTimeout(exitTimerRef.current);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const withCoords = React.useMemo(() => providers.filter((p) => p.coords), [providers]);
|
|
|
|
// External selection (e.g. list hover) force-opens a popup. Internal click wins.
|
|
const effectiveProviderId = activeProviderId ?? selectedProviderId ?? null;
|
|
|
|
const activeProvider = React.useMemo(
|
|
() =>
|
|
effectiveProviderId ? (withCoords.find((p) => p.id === effectiveProviderId) ?? null) : null,
|
|
[withCoords, effectiveProviderId],
|
|
);
|
|
|
|
// Pins hidden from the map (because their popup is showing instead).
|
|
const hiddenIds = React.useMemo(() => {
|
|
const s = new Set<string>();
|
|
if (effectiveProviderId) s.add(effectiveProviderId);
|
|
if (activeCluster) {
|
|
activeCluster.providers.forEach((p) => s.add(p.id));
|
|
}
|
|
return s;
|
|
}, [effectiveProviderId, activeCluster]);
|
|
|
|
const handlePinClick = React.useCallback(
|
|
(id: string) => {
|
|
cancelExit();
|
|
setActiveProviderId(id);
|
|
setActiveCluster(null);
|
|
},
|
|
[cancelExit],
|
|
);
|
|
|
|
const handleClusterClick = React.useCallback(
|
|
(clusterProviders: ProviderData[], position: google.maps.LatLngLiteral) => {
|
|
cancelExit();
|
|
setActiveProviderId(null);
|
|
setActiveCluster({ providers: clusterProviders, position });
|
|
},
|
|
[cancelExit],
|
|
);
|
|
|
|
/** Shared close path — animate the popup out (exiting=true triggers the
|
|
* CSS transition in MapPopup / ClusterPopup), then actually clear state
|
|
* after the transition completes so the pin can fade back in. */
|
|
const closeWithExit = React.useCallback(() => {
|
|
if (!activeProviderId && !activeCluster) return;
|
|
if (exitTimerRef.current) window.clearTimeout(exitTimerRef.current);
|
|
setExiting(true);
|
|
exitTimerRef.current = window.setTimeout(() => {
|
|
setActiveProviderId(null);
|
|
setActiveCluster(null);
|
|
setExiting(false);
|
|
exitTimerRef.current = null;
|
|
}, POPUP_EXIT_MS);
|
|
}, [activeProviderId, activeCluster]);
|
|
|
|
const handleMapClick = closeWithExit;
|
|
const handleCloseCluster = closeWithExit;
|
|
|
|
// Emit active-state changes when the caller is rendering popups externally.
|
|
const onActiveChangeRef = React.useRef(onActiveChange);
|
|
React.useEffect(() => {
|
|
onActiveChangeRef.current = onActiveChange;
|
|
}, [onActiveChange]);
|
|
React.useEffect(() => {
|
|
onActiveChangeRef.current?.({
|
|
provider: activeProvider,
|
|
cluster: activeCluster
|
|
? {
|
|
providers: activeCluster.providers,
|
|
position: {
|
|
lat: activeCluster.position.lat,
|
|
lng: activeCluster.position.lng,
|
|
},
|
|
}
|
|
: null,
|
|
exiting,
|
|
});
|
|
}, [activeProvider, activeCluster, exiting]);
|
|
|
|
/** Cluster list → single-provider drill-in.
|
|
* Pans + zooms the map to the provider's coords (zoom 15 = past
|
|
* CLUSTER_MAX_ZOOM so nearby cluster members separate into individual
|
|
* pins around the selected one), then clears the cluster state and
|
|
* opens the single-provider popup. */
|
|
const handleDrillIntoProvider = React.useCallback(
|
|
(id: string) => {
|
|
cancelExit();
|
|
const provider = withCoords.find((p) => p.id === id);
|
|
if (provider?.coords && mapRef.current) {
|
|
mapRef.current.panTo(provider.coords);
|
|
mapRef.current.setZoom(DRILL_IN_ZOOM);
|
|
}
|
|
setActiveProviderId(id);
|
|
setActiveCluster(null);
|
|
},
|
|
[withCoords, cancelExit],
|
|
);
|
|
|
|
// Imperative handle for external callers (drawer close, cluster-row tap).
|
|
React.useImperativeHandle(
|
|
ref,
|
|
() => ({
|
|
clearActive: closeWithExit,
|
|
drillIntoProvider: handleDrillIntoProvider,
|
|
}),
|
|
[closeWithExit, handleDrillIntoProvider],
|
|
);
|
|
|
|
const rootSx = [
|
|
{
|
|
position: 'relative' as const,
|
|
display: 'flex',
|
|
flex: 1,
|
|
minHeight: 300,
|
|
width: '100%',
|
|
overflow: 'hidden',
|
|
bgcolor: 'var(--fa-color-surface-cool)',
|
|
},
|
|
...(Array.isArray(sx) ? sx : [sx]),
|
|
];
|
|
|
|
// Empty states
|
|
if (!apiKey) {
|
|
return (
|
|
<Box role="application" aria-label="Provider map" sx={rootSx}>
|
|
<MapEmptyState reason="no-key" />
|
|
</Box>
|
|
);
|
|
}
|
|
if (withCoords.length === 0) {
|
|
return (
|
|
<Box role="application" aria-label="Provider map" sx={rootSx}>
|
|
<MapEmptyState reason="no-coords" />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box ref={ref} role="application" aria-label="Provider map" sx={rootSx}>
|
|
<APIProvider apiKey={apiKey}>
|
|
<GoogleMap
|
|
defaultCenter={defaultCenter}
|
|
defaultZoom={defaultZoom}
|
|
mapId={MAP_ID}
|
|
disableDefaultUI
|
|
zoomControl
|
|
gestureHandling="greedy"
|
|
onClick={handleMapClick}
|
|
style={{ width: '100%', height: '100%' }}
|
|
>
|
|
<FitBounds providers={withCoords} />
|
|
<MapRefCapture mapRef={mapRef} />
|
|
|
|
<MarkerLayer
|
|
providers={withCoords}
|
|
hiddenIds={hiddenIds}
|
|
onPinClick={handlePinClick}
|
|
onClusterClick={handleClusterClick}
|
|
/>
|
|
|
|
{/* Internal popups — skipped when caller externalises them (e.g.
|
|
mobile drawer). Active state still flows via onActiveChange. */}
|
|
{!externalisePopups && activeProvider && (
|
|
<AdvancedMarker position={activeProvider.coords!} zIndex={1000}>
|
|
<MapPopup
|
|
name={activeProvider.name}
|
|
imageUrl={activeProvider.imageUrl}
|
|
price={activeProvider.startingPrice}
|
|
location={activeProvider.location}
|
|
rating={activeProvider.rating}
|
|
verified={activeProvider.verified}
|
|
exiting={exiting}
|
|
onClick={() => onSelectProvider(activeProvider.id)}
|
|
/>
|
|
</AdvancedMarker>
|
|
)}
|
|
|
|
{/* Cluster list popup — shown while a cluster is active and no
|
|
provider has been drilled into. Drilling clears activeCluster,
|
|
which swaps this for the single-provider popup above. */}
|
|
{!externalisePopups && activeCluster && !activeProviderId && (
|
|
<AdvancedMarker position={activeCluster.position} zIndex={1000}>
|
|
<ClusterPopup
|
|
providers={activeCluster.providers.map((p) => ({
|
|
id: p.id,
|
|
name: p.name,
|
|
location: p.location,
|
|
verified: p.verified,
|
|
rating: p.rating,
|
|
startingPrice: p.startingPrice,
|
|
}))}
|
|
exiting={exiting}
|
|
onSelectProvider={handleDrillIntoProvider}
|
|
onClose={handleCloseCluster}
|
|
/>
|
|
</AdvancedMarker>
|
|
)}
|
|
</GoogleMap>
|
|
</APIProvider>
|
|
</Box>
|
|
);
|
|
},
|
|
);
|
|
|
|
ProviderMap.displayName = 'ProviderMap';
|
|
export default ProviderMap;
|