From abd8d2d2da92aa9da76165fb96ad725e2eaec014 Mon Sep 17 00:00:00 2001 From: Richie Date: Fri, 22 May 2026 11:53:25 +1000 Subject: [PATCH] ProviderMap: verified providers always show as MapPopup; drawer + list polish Verified providers render as always-on MapPopup cards inside the marker layer (clusterable), while unverified keep click-to-reveal pins. Mobile taps open the drawer instead of navigating directly. MapProviderDrawer gets a "View Packages" CTA button, light-grey header bar, and the provider list doubles card spacing. Fixes demo:dev --mode flag. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- .../MapProviderDrawer/MapProviderDrawer.tsx | 40 ++++---- .../organisms/ProviderMap/ProviderMap.tsx | 96 +++++++++++++------ .../pages/ProvidersStep/ProvidersStep.tsx | 2 +- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index a94aac2..cc8333b 100644 --- a/package.json +++ b/package.json @@ -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", + "demo:dev": "vite -c vite.demo.config.ts --mode arrangement", "demo:build": "vite build -c vite.demo.config.ts", "demo:publish": "npm run demo:build -- --mode arrangement && ./scripts/deploy-demo.sh arrangement", "prepare": "husky" diff --git a/src/components/molecules/MapProviderDrawer/MapProviderDrawer.tsx b/src/components/molecules/MapProviderDrawer/MapProviderDrawer.tsx index 9e3f44d..f5538b8 100644 --- a/src/components/molecules/MapProviderDrawer/MapProviderDrawer.tsx +++ b/src/components/molecules/MapProviderDrawer/MapProviderDrawer.tsx @@ -6,6 +6,7 @@ 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'; @@ -198,6 +199,9 @@ export const MapProviderDrawer = React.forwardRef {cluster && !provider && ( @@ -221,23 +225,27 @@ export const MapProviderDrawer = React.forwardRef - {/* Single-provider content — entire card clickable. Card runs - edge-to-edge with all corners squared; the drawer Paper provides - the top radius. */} + {/* Single-provider content — card is display-only; a CTA button + below handles navigation to the provider's packages. */} {provider && ( - 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 */} diff --git a/src/components/organisms/ProviderMap/ProviderMap.tsx b/src/components/organisms/ProviderMap/ProviderMap.tsx index 0a74758..427b746 100644 --- a/src/components/organisms/ProviderMap/ProviderMap.tsx +++ b/src/components/organisms/ProviderMap/ProviderMap.tsx @@ -2,6 +2,7 @@ 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, @@ -139,20 +140,33 @@ const MapRefCapture: React.FC<{ const MarkerLayer: React.FC<{ providers: ProviderData[]; hiddenIds: Set; + theme: Theme; + externalisePopups: boolean; onPinClick: (id: string) => void; + onSelectProvider: (id: string) => void; onClusterClick: (providers: ProviderData[], position: google.maps.LatLngLiteral) => void; -}> = ({ providers, hiddenIds, onPinClick, onClusterClick }) => { +}> = ({ + providers, + hiddenIds, + theme, + externalisePopups, + onPinClick, + onSelectProvider, + 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, onClusterClick]); + }, [onPinClick, onSelectProvider, onClusterClick]); React.useEffect(() => { if (!map || !markerLibrary) return; @@ -165,20 +179,38 @@ const MarkerLayer: React.FC<{ .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( - { - e.stopPropagation(); - onPinClickRef.current(p.id); - }} - />, - ); + + if (p.verified) { + root.render( + + + externalisePopups + ? onPinClickRef.current(p.id) + : onSelectProviderRef.current(p.id) + } + /> + , + ); + } else { + root.render( + { + e.stopPropagation(); + onPinClickRef.current(p.id); + }} + />, + ); + } roots.push(root); const marker = new markerLibrary.AdvancedMarkerElement({ @@ -186,13 +218,12 @@ const MarkerLayer: React.FC<{ 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); - }); + if (!p.verified) { + marker.addListener('click', (event: google.maps.MapMouseEvent) => { + event.stop(); + onPinClickRef.current(p.id); + }); + } markerToProvider.set(marker, p); return marker; }); @@ -319,6 +350,7 @@ export const ProviderMap = React.forwardRef }, ref, ) => { + const muiTheme = useTheme(); const [activeProviderId, setActiveProviderId] = React.useState(null); const [activeCluster, setActiveCluster] = React.useState(null); const [exiting, setExiting] = React.useState(false); @@ -354,14 +386,18 @@ export const ProviderMap = React.forwardRef ); // 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(); - if (effectiveProviderId) s.add(effectiveProviderId); + if (effectiveProviderId) { + const p = withCoords.find((prov) => prov.id === effectiveProviderId); + if (p && !p.verified) s.add(effectiveProviderId); + } if (activeCluster) { activeCluster.providers.forEach((p) => s.add(p.id)); } return s; - }, [effectiveProviderId, activeCluster]); + }, [effectiveProviderId, activeCluster, withCoords]); const handlePinClick = React.useCallback( (id: string) => { @@ -497,13 +533,17 @@ export const ProviderMap = React.forwardRef - {/* Internal popups — skipped when caller externalises them (e.g. - mobile drawer). Active state still flows via onActiveChange. */} - {!externalisePopups && activeProvider && ( + {/* 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 && ( = ({ sx={{ display: 'flex', flexDirection: 'column', - gap: 2, + gap: 4, pb: 3, pt: 2, px: { xs: 2, md: 3 },