Add ProviderCard molecule — first molecule in design system
First molecule component. Listing card for funeral providers on the provider select screen (map + scrollable list layout). - Verified providers: hero image, 48px logo overlay, "Trusted Partner" badge, name, location, reviews, capability badge, footer with price - Unverified providers: text-only with same content alignment - 7 component tokens (image height, logo size, footer/content spacing) - Composes Card (interactive, padding="none") + Badge + Typography - Capability badges accept arbitrary label + colour (categories may change) - Footer bar with warm brand.100 bg, "Packages from $X >" - 9 Storybook stories including mixed list layout demo - Decisions D026-D030 logged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
420
src/components/molecules/ProviderCard/ProviderCard.stories.tsx
Normal file
420
src/components/molecules/ProviderCard/ProviderCard.stories.tsx
Normal file
@@ -0,0 +1,420 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProviderCard } from './ProviderCard';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// Placeholder images for stories (grey boxes via data URIs)
|
||||
const HERO_PLACEHOLDER =
|
||||
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=600&h=300&fit=crop&auto=format';
|
||||
const HERO_PLACEHOLDER_2 =
|
||||
'https://images.unsplash.com/photo-1497366216548-37526070297c?w=600&h=300&fit=crop&auto=format';
|
||||
const HERO_PLACEHOLDER_3 =
|
||||
'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=600&h=300&fit=crop&auto=format';
|
||||
// Simple grey circle for logo placeholder
|
||||
const LOGO_PLACEHOLDER =
|
||||
'data:image/svg+xml,' +
|
||||
encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96"><circle cx="48" cy="48" r="48" fill="%23E8E8E8"/><text x="48" y="54" text-anchor="middle" font-family="sans-serif" font-size="14" fill="%23737373">Logo</text></svg>',
|
||||
);
|
||||
|
||||
const meta: Meta<typeof ProviderCard> = {
|
||||
title: 'Molecules/ProviderCard',
|
||||
component: ProviderCard,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=5369-140263',
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
name: { control: 'text' },
|
||||
location: { control: 'text' },
|
||||
verified: { control: 'boolean' },
|
||||
rating: { control: { type: 'number', min: 0, max: 5, step: 0.1 } },
|
||||
reviewCount: { control: 'number' },
|
||||
capabilityLabel: { control: 'text' },
|
||||
capabilityColor: {
|
||||
control: 'select',
|
||||
options: ['default', 'success', 'warning', 'error', 'info'],
|
||||
},
|
||||
startingPrice: { control: 'number' },
|
||||
onClick: { action: 'clicked' },
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ width: 380 }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ProviderCard>;
|
||||
|
||||
// ─── Default ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Default — verified provider with all fields */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: 'H.Parsons Funeral Directors',
|
||||
location: 'Wollongong',
|
||||
verified: true,
|
||||
imageUrl: HERO_PLACEHOLDER,
|
||||
logoUrl: LOGO_PLACEHOLDER,
|
||||
rating: 4.8,
|
||||
reviewCount: 127,
|
||||
capabilityLabel: 'Online Arrangement',
|
||||
capabilityColor: 'success',
|
||||
startingPrice: 900,
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Verified Provider ──────────────────────────────────────────────────────
|
||||
|
||||
/** Full verified provider card with all elements */
|
||||
export const VerifiedProvider: Story = {
|
||||
name: 'Verified Provider',
|
||||
render: () => (
|
||||
<ProviderCard
|
||||
name="Parsons Ladies Funeral Directors"
|
||||
location="Wollongong"
|
||||
verified
|
||||
imageUrl={HERO_PLACEHOLDER}
|
||||
logoUrl={LOGO_PLACEHOLDER}
|
||||
rating={4.8}
|
||||
reviewCount={127}
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={900}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Unverified Provider ────────────────────────────────────────────────────
|
||||
|
||||
/** Unverified provider — text only, no image/logo/trusted badge */
|
||||
export const UnverifiedProvider: Story = {
|
||||
name: 'Unverified Provider',
|
||||
render: () => (
|
||||
<ProviderCard
|
||||
name="Rankins Funeral's Heathcote"
|
||||
location="Heathcote"
|
||||
rating={4.2}
|
||||
reviewCount={43}
|
||||
capabilityLabel="Outsourced"
|
||||
capabilityColor="default"
|
||||
startingPrice={1200}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── List Layout ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Mixed verified and unverified providers in a scrollable list.
|
||||
* This is the primary use case — text alignment should be consistent
|
||||
* across both card types for scan readability.
|
||||
*/
|
||||
export const ListLayout: Story = {
|
||||
name: 'List Layout — Mixed',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ width: 400, maxHeight: 700, overflow: 'auto' }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<ProviderCard
|
||||
name="Parsons Ladies Funeral Directors"
|
||||
location="Wollongong"
|
||||
verified
|
||||
imageUrl={HERO_PLACEHOLDER}
|
||||
logoUrl={LOGO_PLACEHOLDER}
|
||||
rating={4.8}
|
||||
reviewCount={127}
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={900}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ProviderCard
|
||||
name="H.Parsons Wollongong"
|
||||
location="Wollongong"
|
||||
verified
|
||||
imageUrl={HERO_PLACEHOLDER_2}
|
||||
logoUrl={LOGO_PLACEHOLDER}
|
||||
rating={4.6}
|
||||
reviewCount={89}
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={1100}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ProviderCard
|
||||
name="Rankins Funeral's Heathcote"
|
||||
location="Heathcote"
|
||||
rating={4.2}
|
||||
reviewCount={43}
|
||||
capabilityLabel="Outsourced"
|
||||
capabilityColor="default"
|
||||
startingPrice={1200}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ProviderCard
|
||||
name="Stella Armois Funerals"
|
||||
location="Corrimal"
|
||||
rating={3.9}
|
||||
reviewCount={18}
|
||||
capabilityLabel="Partial Arrangement"
|
||||
capabilityColor="warning"
|
||||
startingPrice={950}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ProviderCard
|
||||
name="St. Angelo of God Funeral Directors"
|
||||
location="Fairy Meadow"
|
||||
verified
|
||||
imageUrl={HERO_PLACEHOLDER_3}
|
||||
logoUrl={LOGO_PLACEHOLDER}
|
||||
rating={4.9}
|
||||
reviewCount={203}
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={1500}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Capability Variants ────────────────────────────────────────────────────
|
||||
|
||||
/** Three capability badge colours */
|
||||
export const CapabilityVariants: Story = {
|
||||
name: 'Capability Variants',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ width: 380 }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<ProviderCard
|
||||
name="Online Provider"
|
||||
location="Sydney"
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={900}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ProviderCard
|
||||
name="Partial Provider"
|
||||
location="Melbourne"
|
||||
capabilityLabel="Partial Arrangement"
|
||||
capabilityColor="warning"
|
||||
startingPrice={1100}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ProviderCard
|
||||
name="Outsourced Provider"
|
||||
location="Brisbane"
|
||||
capabilityLabel="Outsourced"
|
||||
capabilityColor="default"
|
||||
startingPrice={800}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Edge Cases ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Edge cases: long name, missing fields, extremes */
|
||||
export const EdgeCases: Story = {
|
||||
name: 'Edge Cases',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ width: 380 }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{/* Long name — tests maxLines truncation */}
|
||||
<ProviderCard
|
||||
name="The Most Honourable and Distinguished Funeral Directors of the Greater Wollongong Metropolitan Area"
|
||||
location="Wollongong"
|
||||
rating={4.5}
|
||||
reviewCount={67}
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={2500}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
{/* No reviews */}
|
||||
<ProviderCard
|
||||
name="New Provider"
|
||||
location="Perth"
|
||||
capabilityLabel="Partial Arrangement"
|
||||
capabilityColor="warning"
|
||||
startingPrice={750}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
{/* No capability badge */}
|
||||
<ProviderCard
|
||||
name="Basic Listing"
|
||||
location="Adelaide"
|
||||
rating={3.2}
|
||||
reviewCount={5}
|
||||
startingPrice={600}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
{/* No price (footer hidden) */}
|
||||
<ProviderCard
|
||||
name="Price Unavailable"
|
||||
location="Darwin"
|
||||
rating={4.0}
|
||||
reviewCount={12}
|
||||
capabilityLabel="Outsourced"
|
||||
capabilityColor="default"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
{/* Minimal — just name and location */}
|
||||
<ProviderCard
|
||||
name="Minimal Card"
|
||||
location="Hobart"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Responsive ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Cards at different viewport widths */
|
||||
export const Responsive: Story = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ width: 'auto' }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<Box sx={{ display: 'flex', gap: 3, alignItems: 'start', flexWrap: 'wrap' }}>
|
||||
{[280, 340, 420].map((width) => (
|
||||
<Box key={width} sx={{ width }}>
|
||||
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>{width}px</Box>
|
||||
<ProviderCard
|
||||
name="H.Parsons Funeral Directors"
|
||||
location="Wollongong"
|
||||
verified
|
||||
imageUrl={HERO_PLACEHOLDER}
|
||||
logoUrl={LOGO_PLACEHOLDER}
|
||||
rating={4.8}
|
||||
reviewCount={127}
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={900}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── On Different Backgrounds ───────────────────────────────────────────────
|
||||
|
||||
/** Cards on white vs grey surfaces */
|
||||
export const OnDifferentBackgrounds: Story = {
|
||||
name: 'On Different Backgrounds',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ width: 'auto' }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Box sx={{ width: 360, p: 3, backgroundColor: 'background.default' }}>
|
||||
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>White surface</Box>
|
||||
<ProviderCard
|
||||
name="H.Parsons"
|
||||
location="Wollongong"
|
||||
verified
|
||||
imageUrl={HERO_PLACEHOLDER}
|
||||
logoUrl={LOGO_PLACEHOLDER}
|
||||
rating={4.8}
|
||||
reviewCount={127}
|
||||
startingPrice={900}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ width: 360, p: 3, backgroundColor: 'var(--fa-color-surface-subtle)' }}>
|
||||
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>Grey surface (neutral.50)</Box>
|
||||
<ProviderCard
|
||||
name="Rankins Funerals"
|
||||
location="Heathcote"
|
||||
rating={4.2}
|
||||
reviewCount={43}
|
||||
capabilityLabel="Outsourced"
|
||||
capabilityColor="default"
|
||||
startingPrice={1200}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Interactive Demo ───────────────────────────────────────────────────────
|
||||
|
||||
/** Click any card — fires onClick in Storybook actions panel */
|
||||
export const InteractiveDemo: Story = {
|
||||
name: 'Interactive — Click to Navigate',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ width: 380 }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<ProviderCard
|
||||
name="Parsons Ladies Funeral Directors"
|
||||
location="Wollongong"
|
||||
verified
|
||||
imageUrl={HERO_PLACEHOLDER}
|
||||
logoUrl={LOGO_PLACEHOLDER}
|
||||
rating={4.8}
|
||||
reviewCount={127}
|
||||
capabilityLabel="Online Arrangement"
|
||||
capabilityColor="success"
|
||||
startingPrice={900}
|
||||
onClick={() => alert('Navigate to Parsons Ladies packages')}
|
||||
/>
|
||||
<ProviderCard
|
||||
name="Rankins Funeral's Heathcote"
|
||||
location="Heathcote"
|
||||
rating={4.2}
|
||||
reviewCount={43}
|
||||
capabilityLabel="Outsourced"
|
||||
capabilityColor="default"
|
||||
startingPrice={1200}
|
||||
onClick={() => alert('Navigate to Rankins packages')}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
};
|
||||
260
src/components/molecules/ProviderCard/ProviderCard.tsx
Normal file
260
src/components/molecules/ProviderCard/ProviderCard.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { Badge } from '../../atoms/Badge';
|
||||
import type { BadgeColor } from '../../atoms/Badge/Badge';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Props for the FA ProviderCard molecule */
|
||||
export interface ProviderCardProps {
|
||||
/** Provider display name */
|
||||
name: string;
|
||||
/** Location text (suburb, city) */
|
||||
location: string;
|
||||
/** Whether this provider is a verified/trusted partner */
|
||||
verified?: boolean;
|
||||
/** Hero image URL — only rendered when verified */
|
||||
imageUrl?: string;
|
||||
/** Provider logo URL — circular overlay on image, only rendered when verified */
|
||||
logoUrl?: string;
|
||||
/** Average rating (e.g. 4.8). Omit to hide reviews. */
|
||||
rating?: number;
|
||||
/** Number of reviews (e.g. 127). Omit to hide review count. */
|
||||
reviewCount?: number;
|
||||
/** Capability badge label (e.g. "Online Arrangement") */
|
||||
capabilityLabel?: string;
|
||||
/** Capability badge colour intent — maps to Badge colour */
|
||||
capabilityColor?: BadgeColor;
|
||||
/** Starting price in dollars (shown in footer as "Packages from $X") */
|
||||
startingPrice?: number;
|
||||
/** Click handler — entire card is clickable */
|
||||
onClick?: () => void;
|
||||
/** MUI sx prop for style overrides */
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const LOGO_SIZE = 'var(--fa-provider-card-logo-size)';
|
||||
const LOGO_OVERLAP = 24; // half of 48px logo, in px
|
||||
const IMAGE_HEIGHT = 'var(--fa-provider-card-image-height)';
|
||||
const CONTENT_PADDING = 'var(--fa-provider-card-content-padding)';
|
||||
const CONTENT_GAP = 'var(--fa-provider-card-content-gap)';
|
||||
const FOOTER_BG = 'var(--fa-provider-card-footer-background)';
|
||||
const FOOTER_PX = 'var(--fa-provider-card-footer-padding-x)';
|
||||
const FOOTER_PY = 'var(--fa-provider-card-footer-padding-y)';
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Provider listing card for the FA design system.
|
||||
*
|
||||
* Displays a funeral provider in the provider select screen's scrollable
|
||||
* list. Supports verified (paid partner) and unverified (scraped listing)
|
||||
* providers with consistent text alignment for scan readability.
|
||||
*
|
||||
* **Verified providers** get a hero image, logo overlay, and "Trusted Partner"
|
||||
* badge. **Unverified providers** show text content only — no image, logo,
|
||||
* or verification badge.
|
||||
*
|
||||
* Composes: Card (interactive, padding="none"), Badge, Typography.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* <ProviderCard
|
||||
* name="H.Parsons Funeral Directors"
|
||||
* location="Wollongong"
|
||||
* verified
|
||||
* imageUrl="/images/parsons-hero.jpg"
|
||||
* logoUrl="/images/parsons-logo.png"
|
||||
* rating={4.8}
|
||||
* reviewCount={127}
|
||||
* capabilityLabel="Online Arrangement"
|
||||
* capabilityColor="success"
|
||||
* startingPrice={900}
|
||||
* onClick={() => navigate(`/providers/parsons`)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
location,
|
||||
verified = false,
|
||||
imageUrl,
|
||||
logoUrl,
|
||||
rating,
|
||||
reviewCount,
|
||||
capabilityLabel,
|
||||
capabilityColor = 'default',
|
||||
startingPrice,
|
||||
onClick,
|
||||
sx,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const showImage = verified && imageUrl;
|
||||
const showLogo = verified && logoUrl;
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
interactive
|
||||
padding="none"
|
||||
onClick={onClick}
|
||||
sx={[
|
||||
{ overflow: 'hidden' },
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
>
|
||||
{/* ── Image area (verified only) ── */}
|
||||
{showImage && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: IMAGE_HEIGHT,
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundColor: 'action.hover', // fallback if image fails
|
||||
}}
|
||||
>
|
||||
{/* Trusted Partner badge */}
|
||||
<Box sx={{ position: 'absolute', top: 12, right: 12 }}>
|
||||
<Badge
|
||||
variant="filled"
|
||||
color="brand"
|
||||
size="small"
|
||||
icon={<VerifiedOutlinedIcon />}
|
||||
>
|
||||
Trusted Partner
|
||||
</Badge>
|
||||
</Box>
|
||||
|
||||
{/* Logo overlay */}
|
||||
{showLogo && (
|
||||
<Box
|
||||
component="img"
|
||||
src={logoUrl}
|
||||
alt={`${name} logo`}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -LOGO_OVERLAP,
|
||||
left: CONTENT_PADDING,
|
||||
width: LOGO_SIZE,
|
||||
height: LOGO_SIZE,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
backgroundColor: 'background.paper',
|
||||
boxShadow: 1,
|
||||
border: '2px solid',
|
||||
borderColor: 'background.paper',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* ── Content area ── */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: CONTENT_GAP,
|
||||
p: CONTENT_PADDING,
|
||||
// Extra top padding when logo overlaps into content
|
||||
...(showLogo && { pt: `calc(${CONTENT_PADDING} + ${LOGO_OVERLAP}px)` }),
|
||||
}}
|
||||
>
|
||||
{/* Provider name */}
|
||||
<Typography variant="h5" maxLines={1}>
|
||||
{name}
|
||||
</Typography>
|
||||
|
||||
{/* Meta row: location + reviews */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{/* Location */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<LocationOnOutlinedIcon
|
||||
sx={{ fontSize: 16, color: 'text.secondary' }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{location}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Reviews */}
|
||||
{rating != null && (
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
||||
aria-label={`Rated ${rating} out of 5${reviewCount != null ? `, ${reviewCount} reviews` : ''}`}
|
||||
>
|
||||
<StarRoundedIcon
|
||||
sx={{ fontSize: 16, color: 'warning.main' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{rating}
|
||||
{reviewCount != null && ` (${reviewCount.toLocaleString('en-AU')})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Capability badge */}
|
||||
{capabilityLabel && (
|
||||
<Box>
|
||||
<Badge color={capabilityColor} size="small">
|
||||
{capabilityLabel}
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* ── Footer bar ── */}
|
||||
{startingPrice != null && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 0.5,
|
||||
backgroundColor: FOOTER_BG,
|
||||
px: FOOTER_PX,
|
||||
py: FOOTER_PY,
|
||||
}}
|
||||
>
|
||||
<Typography variant="label" color="text.secondary">
|
||||
Packages from
|
||||
</Typography>
|
||||
<Typography variant="label" sx={{ fontWeight: 700 }}>
|
||||
${startingPrice.toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
<ChevronRightIcon
|
||||
sx={{ fontSize: 20, color: 'text.secondary' }}
|
||||
aria-hidden
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ProviderCard.displayName = 'ProviderCard';
|
||||
export default ProviderCard;
|
||||
2
src/components/molecules/ProviderCard/index.ts
Normal file
2
src/components/molecules/ProviderCard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ProviderCard, default } from './ProviderCard';
|
||||
export type { ProviderCardProps } from './ProviderCard';
|
||||
Reference in New Issue
Block a user