diff --git a/src/components/molecules/MapPopup/MapPopup.stories.tsx b/src/components/molecules/MapPopup/MapPopup.stories.tsx new file mode 100644 index 0000000..b8a63b1 --- /dev/null +++ b/src/components/molecules/MapPopup/MapPopup.stories.tsx @@ -0,0 +1,130 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { MapPopup } from './MapPopup'; +import { MapPin } from '../../atoms/MapPin'; + +// Placeholder images +const IMG_PROVIDER = + 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=200&fit=crop&auto=format'; +const IMG_VENUE = + 'https://images.unsplash.com/photo-1548625149-fc4a29cf7092?w=400&h=200&fit=crop&auto=format'; + +const meta: Meta = { + title: 'Molecules/MapPopup', + component: MapPopup, + tags: ['autodocs'], + parameters: { + layout: 'centered', + backgrounds: { + default: 'map', + values: [{ name: 'map', value: '#E5E3DF' }], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +/** Verified provider with image, price, location, and rating */ +export const VerifiedProvider: Story = { + args: { + name: 'H.Parsons Funeral Directors', + imageUrl: IMG_PROVIDER, + price: 900, + location: 'Wollongong', + rating: 4.8, + verified: true, + onViewDetails: () => {}, + }, +}; + +/** Unverified provider — no image, no badge */ +export const UnverifiedProvider: Story = { + args: { + name: 'Smith & Sons Funeral Services', + price: 1200, + location: 'Sutherland', + onViewDetails: () => {}, + }, +}; + +/** Venue popup — capacity instead of rating */ +export const Venue: Story = { + args: { + name: 'Albany Creek Memorial Park — Garden Chapel', + imageUrl: IMG_VENUE, + price: 450, + location: 'Albany Creek', + capacity: 120, + onViewDetails: () => {}, + }, +}; + +/** Minimal — just name and view details */ +export const Minimal: Story = { + args: { + name: 'Local Funeral Provider', + onViewDetails: () => {}, + }, +}; + +/** No view details link — display only */ +export const DisplayOnly: Story = { + args: { + name: 'H.Parsons Funeral Directors', + imageUrl: IMG_PROVIDER, + price: 900, + location: 'Wollongong', + verified: true, + }, +}; + +/** Custom price label */ +export const CustomPriceLabel: Story = { + args: { + name: 'Premium Funeral Services', + imageUrl: IMG_PROVIDER, + priceLabel: 'Price on application', + location: 'Sydney CBD', + verified: true, + onViewDetails: () => {}, + }, +}; + +/** Pin + Popup composition — shows how they work together on a map */ +export const WithPin: Story = { + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + <> + {}} + /> + + + ), +}; diff --git a/src/components/molecules/MapPopup/MapPopup.tsx b/src/components/molecules/MapPopup/MapPopup.tsx new file mode 100644 index 0000000..576a37a --- /dev/null +++ b/src/components/molecules/MapPopup/MapPopup.tsx @@ -0,0 +1,255 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; +import StarRoundedIcon from '@mui/icons-material/StarRounded'; +import PeopleOutlinedIcon from '@mui/icons-material/PeopleOutlined'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { Typography } from '../../atoms/Typography'; +import { Badge } from '../../atoms/Badge'; +import { Link } from '../../atoms/Link'; +import type { BadgeProps } from '../../atoms/Badge/Badge'; + +// ─── Types ────────────────────────────────────────────────────────────────── + +/** Props for the FA MapPopup molecule */ +export interface MapPopupProps { + /** Provider/venue name */ + name: string; + /** Hero image URL */ + imageUrl?: string; + /** Price in dollars */ + price?: number; + /** Custom price label (e.g. "POA") — overrides formatted price */ + priceLabel?: string; + /** Location text (suburb, city) */ + location?: string; + /** Average rating (e.g. 4.8) */ + rating?: number; + /** Venue capacity */ + capacity?: number; + /** Whether this provider is verified */ + verified?: boolean; + /** Badge colour for the verified badge */ + badgeColor?: BadgeProps['color']; + /** "View details" click handler */ + onViewDetails?: () => void; + /** MUI sx prop for the root Paper */ + sx?: SxProps; +} + +// ─── Constants ────────────────────────────────────────────────────────────── + +const POPUP_WIDTH = 260; +const IMAGE_HEIGHT = 100; +const NUB_SIZE = 8; + +// ─── Component ────────────────────────────────────────────────────────────── + +/** + * Map popup card for the FA design system. + * + * Floating card anchored to a MapPin on click. Shows a compact + * preview of a provider or venue — image, name, price, meta row, + * and a "View details" link. A downward nub visually connects the + * popup to the pin below. + * + * Designed for use as a custom popup in Mapbox GL / Google Maps. + * The parent map container handles positioning; this component + * handles content and styling only. + * + * Composes: Paper + Typography + Badge + Link. + * + * Usage: + * ```tsx + * selectProvider(id)} + * /> + * ``` + */ +export const MapPopup = React.forwardRef( + ( + { + name, + imageUrl, + price, + priceLabel, + location, + rating, + capacity, + verified = false, + badgeColor = 'brand', + onViewDetails, + sx, + }, + ref, + ) => { + const hasMeta = location != null || rating != null || capacity != null; + const hasPrice = price != null || priceLabel != null; + + return ( + + + {/* ── Image ── */} + {imageUrl && ( + + {/* Verified badge inside image */} + {verified && ( + + + Verified + + + )} + + )} + + {/* ── Content ── */} + + {/* Name */} + + {name} + + + {/* Price */} + {hasPrice && ( + + {priceLabel ? ( + + {priceLabel} + + ) : ( + <> + + From + + + ${price!.toLocaleString('en-AU')} + + + )} + + )} + + {/* Meta row */} + {hasMeta && ( + + {location && ( + + + + {location} + + + )} + + {rating != null && ( + + + + {rating} + + + )} + + {capacity != null && ( + + + + {capacity} + + + )} + + )} + + {/* Verified badge (no image fallback) */} + {verified && !imageUrl && ( + + Verified + + )} + + {/* View details link */} + {onViewDetails && ( + { + e.stopPropagation(); + onViewDetails(); + }} + sx={{ mt: 0.5, alignSelf: 'flex-start' }} + > + View details + + )} + + + + {/* Nub — downward pointer connecting to pin */} + + + ); + }, +); + +MapPopup.displayName = 'MapPopup'; +export default MapPopup; diff --git a/src/components/molecules/MapPopup/index.ts b/src/components/molecules/MapPopup/index.ts new file mode 100644 index 0000000..d4fa4af --- /dev/null +++ b/src/components/molecules/MapPopup/index.ts @@ -0,0 +1,2 @@ +export { MapPopup, default } from './MapPopup'; +export type { MapPopupProps } from './MapPopup';