Compare commits
1 Commits
main
...
3d248d1197
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d248d1197 |
@@ -19,7 +19,7 @@
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"chromatic": "chromatic --exit-zero-on-changes --build-script-name=build:storybook",
|
||||
"demo:dev": "vite -c vite.demo.config.ts --mode arrangement",
|
||||
"demo:dev": "vite -c vite.demo.config.ts",
|
||||
"demo:build": "vite build -c vite.demo.config.ts",
|
||||
"demo:publish": "npm run demo:build -- --mode arrangement && ./scripts/deploy-demo.sh arrangement",
|
||||
"prepare": "husky"
|
||||
|
||||
@@ -6,7 +6,6 @@ import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Button } from '../../atoms/Button';
|
||||
import { IconButton } from '../../atoms/IconButton';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { ProviderCard } from '../ProviderCard';
|
||||
@@ -199,9 +198,6 @@ export const MapProviderDrawer = React.forwardRef<HTMLDivElement, MapProviderDra
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
gap: 1,
|
||||
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
}}
|
||||
>
|
||||
{cluster && !provider && (
|
||||
@@ -225,27 +221,23 @@ export const MapProviderDrawer = React.forwardRef<HTMLDivElement, MapProviderDra
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Single-provider content — card is display-only; a CTA button
|
||||
below handles navigation to the provider's packages. */}
|
||||
{/* Single-provider content — entire card clickable. Card runs
|
||||
edge-to-edge with all corners squared; the drawer Paper provides
|
||||
the top radius. */}
|
||||
{provider && (
|
||||
<Box>
|
||||
<ProviderCard
|
||||
name={provider.name}
|
||||
location={provider.location}
|
||||
verified={provider.verified}
|
||||
imageUrl={provider.imageUrl}
|
||||
logoUrl={provider.logoUrl}
|
||||
rating={provider.rating}
|
||||
reviewCount={provider.reviewCount}
|
||||
startingPrice={provider.startingPrice}
|
||||
sx={{ borderRadius: 0, boxShadow: 'none', border: 'none' }}
|
||||
/>
|
||||
<Box sx={{ px: 2, pb: 2, pt: 1 }}>
|
||||
<Button variant="contained" fullWidth onClick={() => onSelectProvider(provider.id)}>
|
||||
View Packages
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<ProviderCard
|
||||
name={provider.name}
|
||||
location={provider.location}
|
||||
verified={provider.verified}
|
||||
imageUrl={provider.imageUrl}
|
||||
logoUrl={provider.logoUrl}
|
||||
rating={provider.rating}
|
||||
reviewCount={provider.reviewCount}
|
||||
startingPrice={provider.startingPrice}
|
||||
onClick={() => onSelectProvider(provider.id)}
|
||||
aria-label={`${provider.name}, ${provider.location}. Tap to view packages.`}
|
||||
sx={{ borderRadius: 0, boxShadow: 'none', border: 'none' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Cluster list content — tap a row to drill in */}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 { ThemeProvider, useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
APIProvider,
|
||||
Map as GoogleMap,
|
||||
@@ -140,33 +139,20 @@ const MapRefCapture: React.FC<{
|
||||
const MarkerLayer: React.FC<{
|
||||
providers: ProviderData[];
|
||||
hiddenIds: Set<string>;
|
||||
theme: Theme;
|
||||
externalisePopups: boolean;
|
||||
onPinClick: (id: string) => void;
|
||||
onSelectProvider: (id: string) => void;
|
||||
onClusterClick: (providers: ProviderData[], position: google.maps.LatLngLiteral) => void;
|
||||
}> = ({
|
||||
providers,
|
||||
hiddenIds,
|
||||
theme,
|
||||
externalisePopups,
|
||||
onPinClick,
|
||||
onSelectProvider,
|
||||
onClusterClick,
|
||||
}) => {
|
||||
}> = ({ 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 onSelectProviderRef = React.useRef(onSelectProvider);
|
||||
const onClusterClickRef = React.useRef(onClusterClick);
|
||||
React.useEffect(() => {
|
||||
onPinClickRef.current = onPinClick;
|
||||
onSelectProviderRef.current = onSelectProvider;
|
||||
onClusterClickRef.current = onClusterClick;
|
||||
}, [onPinClick, onSelectProvider, onClusterClick]);
|
||||
}, [onPinClick, onClusterClick]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!map || !markerLibrary) return;
|
||||
@@ -179,38 +165,20 @@ const MarkerLayer: React.FC<{
|
||||
.map((p) => {
|
||||
const el = document.createElement('div');
|
||||
const root = createRoot(el);
|
||||
|
||||
if (p.verified) {
|
||||
root.render(
|
||||
<ThemeProvider theme={theme}>
|
||||
<MapPopup
|
||||
name={p.name}
|
||||
imageUrl={p.imageUrl}
|
||||
price={p.startingPrice}
|
||||
location={p.location}
|
||||
rating={p.rating}
|
||||
verified
|
||||
onClick={() =>
|
||||
externalisePopups
|
||||
? onPinClickRef.current(p.id)
|
||||
: onSelectProviderRef.current(p.id)
|
||||
}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
} else {
|
||||
root.render(
|
||||
<MapPin
|
||||
name={p.name}
|
||||
price={p.startingPrice}
|
||||
verified={p.verified}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPinClickRef.current(p.id);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
// 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({
|
||||
@@ -218,12 +186,13 @@ const MarkerLayer: React.FC<{
|
||||
content: el,
|
||||
gmpClickable: true,
|
||||
});
|
||||
if (!p.verified) {
|
||||
marker.addListener('click', (event: google.maps.MapMouseEvent) => {
|
||||
event.stop();
|
||||
onPinClickRef.current(p.id);
|
||||
});
|
||||
}
|
||||
// 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;
|
||||
});
|
||||
@@ -350,7 +319,6 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const muiTheme = useTheme();
|
||||
const [activeProviderId, setActiveProviderId] = React.useState<string | null>(null);
|
||||
const [activeCluster, setActiveCluster] = React.useState<ActiveCluster | null>(null);
|
||||
const [exiting, setExiting] = React.useState(false);
|
||||
@@ -386,18 +354,14 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
|
||||
);
|
||||
|
||||
// Pins hidden from the map (because their popup is showing instead).
|
||||
// Verified providers are excluded — their marker IS the MapPopup.
|
||||
const hiddenIds = React.useMemo(() => {
|
||||
const s = new Set<string>();
|
||||
if (effectiveProviderId) {
|
||||
const p = withCoords.find((prov) => prov.id === effectiveProviderId);
|
||||
if (p && !p.verified) s.add(effectiveProviderId);
|
||||
}
|
||||
if (effectiveProviderId) s.add(effectiveProviderId);
|
||||
if (activeCluster) {
|
||||
activeCluster.providers.forEach((p) => s.add(p.id));
|
||||
}
|
||||
return s;
|
||||
}, [effectiveProviderId, activeCluster, withCoords]);
|
||||
}, [effectiveProviderId, activeCluster]);
|
||||
|
||||
const handlePinClick = React.useCallback(
|
||||
(id: string) => {
|
||||
@@ -533,17 +497,13 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
|
||||
<MarkerLayer
|
||||
providers={withCoords}
|
||||
hiddenIds={hiddenIds}
|
||||
theme={muiTheme}
|
||||
externalisePopups={externalisePopups}
|
||||
onPinClick={handlePinClick}
|
||||
onSelectProvider={onSelectProvider}
|
||||
onClusterClick={handleClusterClick}
|
||||
/>
|
||||
|
||||
{/* Click-to-reveal popup for unverified providers. Verified
|
||||
providers are always rendered as MapPopup inside MarkerLayer,
|
||||
so they don't need this path. */}
|
||||
{!externalisePopups && activeProvider && !activeProvider.verified && (
|
||||
{/* 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}
|
||||
|
||||
@@ -807,7 +807,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
gap: 2,
|
||||
pb: 3,
|
||||
pt: 2,
|
||||
px: { xs: 2, md: 3 },
|
||||
|
||||
Reference in New Issue
Block a user