Compare commits
2 Commits
3d248d1197
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 62a9db4e64 | |||
| abd8d2d2da |
@@ -19,7 +19,7 @@
|
|||||||
"test": "vitest run --passWithNoTests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"chromatic": "chromatic --exit-zero-on-changes --build-script-name=build:storybook",
|
"chromatic": "chromatic --exit-zero-on-changes --build-script-name=build:storybook",
|
||||||
"demo:dev": "vite -c vite.demo.config.ts",
|
"demo:dev": "vite -c vite.demo.config.ts --mode arrangement",
|
||||||
"demo:build": "vite build -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",
|
"demo:publish": "npm run demo:build -- --mode arrangement && ./scripts/deploy-demo.sh arrangement",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
|||||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
import { IconButton } from '../../atoms/IconButton';
|
import { IconButton } from '../../atoms/IconButton';
|
||||||
import { Typography } from '../../atoms/Typography';
|
import { Typography } from '../../atoms/Typography';
|
||||||
import { ProviderCard } from '../ProviderCard';
|
import { ProviderCard } from '../ProviderCard';
|
||||||
@@ -198,6 +199,9 @@ export const MapProviderDrawer = React.forwardRef<HTMLDivElement, MapProviderDra
|
|||||||
px: 2,
|
px: 2,
|
||||||
py: 0.5,
|
py: 0.5,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
|
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cluster && !provider && (
|
{cluster && !provider && (
|
||||||
@@ -221,23 +225,27 @@ export const MapProviderDrawer = React.forwardRef<HTMLDivElement, MapProviderDra
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Single-provider content — entire card clickable. Card runs
|
{/* Single-provider content — card is display-only; a CTA button
|
||||||
edge-to-edge with all corners squared; the drawer Paper provides
|
below handles navigation to the provider's packages. */}
|
||||||
the top radius. */}
|
|
||||||
{provider && (
|
{provider && (
|
||||||
<ProviderCard
|
<Box>
|
||||||
name={provider.name}
|
<ProviderCard
|
||||||
location={provider.location}
|
name={provider.name}
|
||||||
verified={provider.verified}
|
location={provider.location}
|
||||||
imageUrl={provider.imageUrl}
|
verified={provider.verified}
|
||||||
logoUrl={provider.logoUrl}
|
imageUrl={provider.imageUrl}
|
||||||
rating={provider.rating}
|
logoUrl={provider.logoUrl}
|
||||||
reviewCount={provider.reviewCount}
|
rating={provider.rating}
|
||||||
startingPrice={provider.startingPrice}
|
reviewCount={provider.reviewCount}
|
||||||
onClick={() => onSelectProvider(provider.id)}
|
startingPrice={provider.startingPrice}
|
||||||
aria-label={`${provider.name}, ${provider.location}. Tap to view packages.`}
|
sx={{ borderRadius: 0, boxShadow: 'none', border: 'none' }}
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cluster list content — tap a row to drill in */}
|
{/* Cluster list content — tap a row to drill in */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { createRoot, type Root } from 'react-dom/client';
|
import { createRoot, type Root } from 'react-dom/client';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { ThemeProvider, useTheme } from '@mui/material/styles';
|
||||||
import {
|
import {
|
||||||
APIProvider,
|
APIProvider,
|
||||||
Map as GoogleMap,
|
Map as GoogleMap,
|
||||||
@@ -139,20 +140,33 @@ const MapRefCapture: React.FC<{
|
|||||||
const MarkerLayer: React.FC<{
|
const MarkerLayer: React.FC<{
|
||||||
providers: ProviderData[];
|
providers: ProviderData[];
|
||||||
hiddenIds: Set<string>;
|
hiddenIds: Set<string>;
|
||||||
|
theme: Theme;
|
||||||
|
externalisePopups: boolean;
|
||||||
onPinClick: (id: string) => void;
|
onPinClick: (id: string) => void;
|
||||||
|
onSelectProvider: (id: string) => void;
|
||||||
onClusterClick: (providers: ProviderData[], position: google.maps.LatLngLiteral) => void;
|
onClusterClick: (providers: ProviderData[], position: google.maps.LatLngLiteral) => void;
|
||||||
}> = ({ providers, hiddenIds, onPinClick, onClusterClick }) => {
|
}> = ({
|
||||||
|
providers,
|
||||||
|
hiddenIds,
|
||||||
|
theme,
|
||||||
|
externalisePopups,
|
||||||
|
onPinClick,
|
||||||
|
onSelectProvider,
|
||||||
|
onClusterClick,
|
||||||
|
}) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const markerLibrary = useMapsLibrary('marker');
|
const markerLibrary = useMapsLibrary('marker');
|
||||||
|
|
||||||
// Stash callbacks in a ref so the effect below doesn't re-run (and rebuild
|
// 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.
|
// every marker) when the parent passes fresh arrow-function references.
|
||||||
const onPinClickRef = React.useRef(onPinClick);
|
const onPinClickRef = React.useRef(onPinClick);
|
||||||
|
const onSelectProviderRef = React.useRef(onSelectProvider);
|
||||||
const onClusterClickRef = React.useRef(onClusterClick);
|
const onClusterClickRef = React.useRef(onClusterClick);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
onPinClickRef.current = onPinClick;
|
onPinClickRef.current = onPinClick;
|
||||||
|
onSelectProviderRef.current = onSelectProvider;
|
||||||
onClusterClickRef.current = onClusterClick;
|
onClusterClickRef.current = onClusterClick;
|
||||||
}, [onPinClick, onClusterClick]);
|
}, [onPinClick, onSelectProvider, onClusterClick]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!map || !markerLibrary) return;
|
if (!map || !markerLibrary) return;
|
||||||
@@ -165,20 +179,38 @@ const MarkerLayer: React.FC<{
|
|||||||
.map((p) => {
|
.map((p) => {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
const root = createRoot(el);
|
const root = createRoot(el);
|
||||||
// MapPin's own onClick stays for keyboard a11y (Enter/Space via its
|
|
||||||
// onKeyDown). stopPropagation guards against the DOM click bubbling
|
if (p.verified) {
|
||||||
// to the Map's onClick and closing the popup the same frame it opens.
|
root.render(
|
||||||
root.render(
|
<ThemeProvider theme={theme}>
|
||||||
<MapPin
|
<MapPopup
|
||||||
name={p.name}
|
name={p.name}
|
||||||
price={p.startingPrice}
|
imageUrl={p.imageUrl}
|
||||||
verified={p.verified}
|
price={p.startingPrice}
|
||||||
onClick={(e) => {
|
location={p.location}
|
||||||
e.stopPropagation();
|
rating={p.rating}
|
||||||
onPinClickRef.current(p.id);
|
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);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
roots.push(root);
|
roots.push(root);
|
||||||
|
|
||||||
const marker = new markerLibrary.AdvancedMarkerElement({
|
const marker = new markerLibrary.AdvancedMarkerElement({
|
||||||
@@ -186,13 +218,12 @@ const MarkerLayer: React.FC<{
|
|||||||
content: el,
|
content: el,
|
||||||
gmpClickable: true,
|
gmpClickable: true,
|
||||||
});
|
});
|
||||||
// Also listen at the Google Maps level + stop the GMaps event so
|
if (!p.verified) {
|
||||||
// Map's onClick can't fire when a pin is clicked via mouse. Safe to
|
marker.addListener('click', (event: google.maps.MapMouseEvent) => {
|
||||||
// fire twice with keyboard — handlePinClick is idempotent.
|
event.stop();
|
||||||
marker.addListener('click', (event: google.maps.MapMouseEvent) => {
|
onPinClickRef.current(p.id);
|
||||||
event.stop();
|
});
|
||||||
onPinClickRef.current(p.id);
|
}
|
||||||
});
|
|
||||||
markerToProvider.set(marker, p);
|
markerToProvider.set(marker, p);
|
||||||
return marker;
|
return marker;
|
||||||
});
|
});
|
||||||
@@ -319,6 +350,7 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
const muiTheme = useTheme();
|
||||||
const [activeProviderId, setActiveProviderId] = React.useState<string | null>(null);
|
const [activeProviderId, setActiveProviderId] = React.useState<string | null>(null);
|
||||||
const [activeCluster, setActiveCluster] = React.useState<ActiveCluster | null>(null);
|
const [activeCluster, setActiveCluster] = React.useState<ActiveCluster | null>(null);
|
||||||
const [exiting, setExiting] = React.useState(false);
|
const [exiting, setExiting] = React.useState(false);
|
||||||
@@ -354,14 +386,18 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Pins hidden from the map (because their popup is showing instead).
|
// 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 hiddenIds = React.useMemo(() => {
|
||||||
const s = new Set<string>();
|
const s = new Set<string>();
|
||||||
if (effectiveProviderId) s.add(effectiveProviderId);
|
if (effectiveProviderId) {
|
||||||
|
const p = withCoords.find((prov) => prov.id === effectiveProviderId);
|
||||||
|
if (p && !p.verified) s.add(effectiveProviderId);
|
||||||
|
}
|
||||||
if (activeCluster) {
|
if (activeCluster) {
|
||||||
activeCluster.providers.forEach((p) => s.add(p.id));
|
activeCluster.providers.forEach((p) => s.add(p.id));
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
}, [effectiveProviderId, activeCluster]);
|
}, [effectiveProviderId, activeCluster, withCoords]);
|
||||||
|
|
||||||
const handlePinClick = React.useCallback(
|
const handlePinClick = React.useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
@@ -497,13 +533,17 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
|
|||||||
<MarkerLayer
|
<MarkerLayer
|
||||||
providers={withCoords}
|
providers={withCoords}
|
||||||
hiddenIds={hiddenIds}
|
hiddenIds={hiddenIds}
|
||||||
|
theme={muiTheme}
|
||||||
|
externalisePopups={externalisePopups}
|
||||||
onPinClick={handlePinClick}
|
onPinClick={handlePinClick}
|
||||||
|
onSelectProvider={onSelectProvider}
|
||||||
onClusterClick={handleClusterClick}
|
onClusterClick={handleClusterClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Internal popups — skipped when caller externalises them (e.g.
|
{/* Click-to-reveal popup for unverified providers. Verified
|
||||||
mobile drawer). Active state still flows via onActiveChange. */}
|
providers are always rendered as MapPopup inside MarkerLayer,
|
||||||
{!externalisePopups && activeProvider && (
|
so they don't need this path. */}
|
||||||
|
{!externalisePopups && activeProvider && !activeProvider.verified && (
|
||||||
<AdvancedMarker position={activeProvider.coords!} zIndex={1000}>
|
<AdvancedMarker position={activeProvider.coords!} zIndex={1000}>
|
||||||
<MapPopup
|
<MapPopup
|
||||||
name={activeProvider.name}
|
name={activeProvider.name}
|
||||||
|
|||||||
@@ -807,7 +807,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 2,
|
gap: 4,
|
||||||
pb: 3,
|
pb: 3,
|
||||||
pt: 2,
|
pt: 2,
|
||||||
px: { xs: 2, md: 3 },
|
px: { xs: 2, md: 3 },
|
||||||
|
|||||||
Reference in New Issue
Block a user