Add LineItem, ProviderCardCompact, PackageDetail for Package Select page
LineItem (molecule): - Name + optional info tooltip + optional price - Allowance asterisk, total variant (bold + top border) - Reusable for package contents, order summaries, invoices ProviderCardCompact (molecule): - Horizontal layout: image left, name + location + rating right - Used at top of Package Select page to show selected provider PackageDetail (organism): - Right-side detail panel for Package Select page - Name/price header, Make Arrangement + Compare CTAs - Grouped LineItem sections, total row, T&C footer - PackageSelectPage story: full page with filter chips, package list (ServiceOption), sticky detail panel, and Navigation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,8 @@ duplicates) and MUST update it after completing one.
|
|||||||
| SearchBar | done | Input + IconButton + Button | Search input with optional submit button. Enter-to-submit, progressive clear button, inline loading spinner. Guards empty submissions, refocuses after clear. role="search" landmark. Critique: 35/40. |
|
| SearchBar | done | Input + IconButton + Button | Search input with optional submit button. Enter-to-submit, progressive clear button, inline loading spinner. Guards empty submissions, refocuses after clear. role="search" landmark. Critique: 35/40. |
|
||||||
| AddOnOption | done | Card (interactive, selected) + Typography + Switch | Toggleable add-on for arrangement flow extras. Heading + optional price + description + Switch. Click-anywhere toggle. Maps to Figma ListItemAddItem (2350:40658). |
|
| AddOnOption | done | Card (interactive, selected) + Typography + Switch | Toggleable add-on for arrangement flow extras. Heading + optional price + description + Switch. Click-anywhere toggle. Maps to Figma ListItemAddItem (2350:40658). |
|
||||||
| StepIndicator | done | Typography + Box | Horizontal segmented progress bar. Brand gold for completed/current steps, grey for upcoming. Responsive bar height (10px/6px). Maps to Figma Progress Bar - Steps (2375:47468). |
|
| StepIndicator | done | Typography + Box | Horizontal segmented progress bar. Brand gold for completed/current steps, grey for upcoming. Responsive bar height (10px/6px). Maps to Figma Progress Bar - Steps (2375:47468). |
|
||||||
|
| LineItem | review | Typography + Tooltip + InfoOutlinedIcon | Name + optional info tooltip + optional price. Supports allowance asterisk, total variant (bold + border). Used in PackageDetail, order summaries, invoices. |
|
||||||
|
| ProviderCardCompact | review | Card (outlined) + Typography | Horizontal compact provider card — image left, name + location + rating right. Used at top of Package Select page. Separate from vertical ProviderCard. |
|
||||||
|
|
||||||
## Organisms
|
## Organisms
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ duplicates) and MUST update it after completing one.
|
|||||||
|-----------|--------|-------------|-------|
|
|-----------|--------|-------------|-------|
|
||||||
| ServiceSelector | done | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. |
|
| ServiceSelector | done | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. |
|
||||||
| PricingTable | planned | PriceCard × n + Typography | Comparative pricing display |
|
| PricingTable | planned | PriceCard × n + Typography | Comparative pricing display |
|
||||||
|
| PackageDetail | review | LineItem × n + Typography + Button + Divider | Right-side package detail panel. Name/price header, Make Arrangement + Compare buttons, grouped LineItem sections (Essentials, Complimentary, Extras), total row, T&C footer. Maps to Figma Package Select (5405:181955) right column. |
|
||||||
| ArrangementForm | planned | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Deferred — build remaining atoms/molecules first. |
|
| ArrangementForm | planned | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Deferred — build remaining atoms/molecules first. |
|
||||||
| Navigation | done | AppBar + Link + IconButton + Button + Divider + Drawer | Responsive site header. Desktop: logo left, links right, optional CTA. Mobile: hamburger + drawer with nav items, CTA, help footer. Sticky, grey surface bg (surface.subtle). Real FA logo from brandassets/. Maps to Figma Main Nav (14:108) + Mobile Header (2391:41508). |
|
| Navigation | done | AppBar + Link + IconButton + Button + Divider + Drawer | Responsive site header. Desktop: logo left, links right, optional CTA. Mobile: hamburger + drawer with nav items, CTA, help footer. Sticky, grey surface bg (surface.subtle). Real FA logo from brandassets/. Maps to Figma Main Nav (14:108) + Mobile Header (2391:41508). |
|
||||||
| Footer | done | Link × n + Typography + Divider + Container + Grid | Dark espresso (brand.950) site footer. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). |
|
| Footer | done | Link × n + Typography + Divider + Container + Grid | Dark espresso (brand.950) site footer. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). |
|
||||||
|
|||||||
129
src/components/molecules/LineItem/LineItem.stories.tsx
Normal file
129
src/components/molecules/LineItem/LineItem.stories.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { LineItem } from './LineItem';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
|
||||||
|
const meta: Meta<typeof LineItem> = {
|
||||||
|
title: 'Molecules/LineItem',
|
||||||
|
component: LineItem,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ maxWidth: 500, width: '100%' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof LineItem>;
|
||||||
|
|
||||||
|
// --- Default -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Basic line item with name and price */
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Professional Service Fee',
|
||||||
|
price: 1500,
|
||||||
|
info: 'Covers coordination of the entire funeral service, liaising with clergy, cemetery, and crematorium.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Allowance ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Price marked with asterisk — indicates an allowance that can be customised */
|
||||||
|
export const Allowance: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Allowance for Coffin',
|
||||||
|
price: 1500,
|
||||||
|
isAllowance: true,
|
||||||
|
info: 'This is an allowance amount. You may upgrade or change the coffin selection during your arrangement.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- No Price ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Complimentary/included item — no price shown */
|
||||||
|
export const Complimentary: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Dressing Fee',
|
||||||
|
info: 'Included at no additional charge with this package.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Custom Price Label ------------------------------------------------------
|
||||||
|
|
||||||
|
/** Custom text instead of dollar amount */
|
||||||
|
export const CustomPriceLabel: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Transfer of Deceased',
|
||||||
|
priceLabel: 'Included',
|
||||||
|
info: 'Transfer within 50km of the funeral home.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Total Row ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Summary total — bold with top border */
|
||||||
|
export const Total: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Total',
|
||||||
|
price: 2700,
|
||||||
|
variant: 'total',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Package Contents --------------------------------------------------------
|
||||||
|
|
||||||
|
/** Realistic package breakdown as seen on the Package Select page */
|
||||||
|
export const PackageContents: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}>
|
||||||
|
Essentials
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
|
<LineItem name="Accommodation" price={1500} info="Refrigerated holding of the deceased prior to the funeral service." />
|
||||||
|
<LineItem name="Death Registration Certificate" price={1500} info="Lodgement of death registration with NSW Registry of Births, Deaths & Marriages." />
|
||||||
|
<LineItem name="Doctor Fee for Cremation" price={1500} info="Statutory medical referee fee required for all cremations in NSW." />
|
||||||
|
<LineItem name="NSW Government Levy — Cremation" price={1500} info="NSW Government cremation levy as set by the Department of Health." />
|
||||||
|
<LineItem name="Professional Mortuary Care" price={1500} info="Preparation and care of the deceased." />
|
||||||
|
<LineItem name="Professional Service Fee" price={1500} info="Coordination of all funeral arrangements and services." />
|
||||||
|
<LineItem name="Allowance for Coffin" price={1500} isAllowance info="Allowance amount — upgrade options available during arrangement." />
|
||||||
|
<LineItem name="Allowance for Crematorium" price={1500} isAllowance info="Allowance for crematorium fees — varies by location." />
|
||||||
|
<LineItem name="Allowance for Hearse" price={1500} isAllowance info="Allowance for hearse transfer — distance surcharges may apply." />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}>
|
||||||
|
Complimentary Items
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
|
<LineItem name="Dressing Fee" info="Dressing and preparation of the deceased — included at no charge." />
|
||||||
|
<LineItem name="Viewing Fee" info="One private family viewing — included at no charge." />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<LineItem name="Total" price={13500} variant="total" />
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}>
|
||||||
|
Extras
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
|
<LineItem name="Allowance for Flowers" price={1500} isAllowance info="Seasonal floral arrangements for the service." />
|
||||||
|
<LineItem name="Allowance for Master of Ceremonies" price={1500} isAllowance info="Professional celebrant or MC for the funeral service." />
|
||||||
|
<LineItem name="After Business Hours Service Surcharge" price={1500} info="Additional fee for services held outside standard business hours." />
|
||||||
|
<LineItem name="After Hours Prayers" price={1500} info="Evening prayer service at the funeral home." />
|
||||||
|
<LineItem name="Coffin Bearing by Funeral Directors" price={1500} info="Professional pallbearing by funeral directors." />
|
||||||
|
<LineItem name="Digital Recording" price={1500} info="Professional video recording of the funeral service." />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
121
src/components/molecules/LineItem/LineItem.tsx
Normal file
121
src/components/molecules/LineItem/LineItem.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA LineItem molecule */
|
||||||
|
export interface LineItemProps {
|
||||||
|
/** Item name/label */
|
||||||
|
name: string;
|
||||||
|
/** Optional tooltip text explaining the item (shown via info icon) */
|
||||||
|
info?: string;
|
||||||
|
/** Price in dollars — omit for complimentary/included items */
|
||||||
|
price?: number;
|
||||||
|
/** Whether the price is an allowance (shows asterisk) */
|
||||||
|
isAllowance?: boolean;
|
||||||
|
/** Custom price display — overrides `price` formatting (e.g. "Included", "TBC") */
|
||||||
|
priceLabel?: string;
|
||||||
|
/** Visual weight — "default" for regular items, "total" for summary rows */
|
||||||
|
variant?: 'default' | 'total';
|
||||||
|
/** MUI sx prop for the root element */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single line item showing a name, optional info tooltip, and optional price.
|
||||||
|
*
|
||||||
|
* Used in package contents, order summaries, and invoices. The `info` prop
|
||||||
|
* renders a small info icon with a tooltip — used by providers to explain
|
||||||
|
* what each inclusion covers.
|
||||||
|
*
|
||||||
|
* Composes Typography + Tooltip.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```tsx
|
||||||
|
* <LineItem name="Professional Service Fee" info="Covers all coordination..." price={1500} />
|
||||||
|
* <LineItem name="Allowance for Coffin" price={1500} isAllowance info="Can be upgraded..." />
|
||||||
|
* <LineItem name="Dressing Fee" info="Included in this package" />
|
||||||
|
* <LineItem name="Total" price={2700} variant="total" />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const LineItem = React.forwardRef<HTMLDivElement, LineItemProps>(
|
||||||
|
({ name, info, price, isAllowance = false, priceLabel, variant = 'default', sx }, ref) => {
|
||||||
|
const isTotal = variant === 'total';
|
||||||
|
|
||||||
|
const formattedPrice = priceLabel
|
||||||
|
?? (price != null ? `$${price.toLocaleString('en-AU')}${isAllowance ? '*' : ''}` : undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
...(isTotal && {
|
||||||
|
pt: 1.5,
|
||||||
|
mt: 1.5,
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Name + optional info icon */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
variant={isTotal ? 'h6' : 'body2'}
|
||||||
|
sx={{
|
||||||
|
fontWeight: isTotal ? 600 : 400,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{info && (
|
||||||
|
<Tooltip title={info} arrow placement="top">
|
||||||
|
<InfoOutlinedIcon
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: 'text.secondary',
|
||||||
|
cursor: 'help',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
{formattedPrice && (
|
||||||
|
<Typography
|
||||||
|
variant={isTotal ? 'h6' : 'body2'}
|
||||||
|
sx={{
|
||||||
|
fontWeight: isTotal ? 600 : 400,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flexShrink: 0,
|
||||||
|
...(isTotal && { color: 'primary.main' }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formattedPrice}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
LineItem.displayName = 'LineItem';
|
||||||
|
export default LineItem;
|
||||||
1
src/components/molecules/LineItem/index.ts
Normal file
1
src/components/molecules/LineItem/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { LineItem, type LineItemProps } from './LineItem';
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { ProviderCardCompact } from './ProviderCardCompact';
|
||||||
|
|
||||||
|
const DEMO_IMAGE = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
|
||||||
|
|
||||||
|
const meta: Meta<typeof ProviderCardCompact> = {
|
||||||
|
title: 'Molecules/ProviderCardCompact',
|
||||||
|
component: ProviderCardCompact,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ maxWidth: 480, width: '100%' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ProviderCardCompact>;
|
||||||
|
|
||||||
|
// --- Default -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Compact provider card with image, name, location, and rating */
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'H.Parsons',
|
||||||
|
location: 'Wentworth',
|
||||||
|
imageUrl: DEMO_IMAGE,
|
||||||
|
rating: 4.5,
|
||||||
|
reviewCount: 11,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Without Image -----------------------------------------------------------
|
||||||
|
|
||||||
|
/** Provider without a photo — text only */
|
||||||
|
export const WithoutImage: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Smith & Sons Funerals',
|
||||||
|
location: 'Parramatta',
|
||||||
|
rating: 4.2,
|
||||||
|
reviewCount: 38,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Without Rating ----------------------------------------------------------
|
||||||
|
|
||||||
|
/** Provider without reviews */
|
||||||
|
export const WithoutRating: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Peaceful Rest Funerals',
|
||||||
|
location: 'Mildura',
|
||||||
|
imageUrl: DEMO_IMAGE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Interactive -------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Clickable — navigates back to provider details */
|
||||||
|
export const Interactive: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'H.Parsons',
|
||||||
|
location: 'Wentworth',
|
||||||
|
imageUrl: DEMO_IMAGE,
|
||||||
|
rating: 4.5,
|
||||||
|
reviewCount: 11,
|
||||||
|
onClick: () => alert('Navigate to provider details'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- In Context --------------------------------------------------------------
|
||||||
|
|
||||||
|
/** As it appears at the top of the Package Select page */
|
||||||
|
export const InContext: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
onClick={() => {}}
|
||||||
|
sx={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
mb: 3,
|
||||||
|
p: 0,
|
||||||
|
color: 'text.secondary',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: '1rem',
|
||||||
|
'&:hover': { color: 'text.primary' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Back
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ typography: 'h3', mb: 3 }}>Select a package</Box>
|
||||||
|
<ProviderCardCompact
|
||||||
|
name="H.Parsons"
|
||||||
|
location="Wentworth"
|
||||||
|
imageUrl={DEMO_IMAGE}
|
||||||
|
rating={4.5}
|
||||||
|
reviewCount={11}
|
||||||
|
onClick={() => alert('View provider')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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 { Card } from '../../atoms/Card';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA ProviderCardCompact molecule */
|
||||||
|
export interface ProviderCardCompactProps {
|
||||||
|
/** Provider display name */
|
||||||
|
name: string;
|
||||||
|
/** Location text (suburb, city) */
|
||||||
|
location: string;
|
||||||
|
/** Hero image URL — shown on the left */
|
||||||
|
imageUrl?: string;
|
||||||
|
/** Average rating (e.g. 4.5). Omit to hide. */
|
||||||
|
rating?: number;
|
||||||
|
/** Number of reviews. Omit to hide review count. */
|
||||||
|
reviewCount?: number;
|
||||||
|
/** Click handler — makes the card interactive */
|
||||||
|
onClick?: () => void;
|
||||||
|
/** MUI sx prop for the root element */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact horizontal provider card for the FA design system.
|
||||||
|
*
|
||||||
|
* Used at the top of the Package Select page to show which provider
|
||||||
|
* the user has selected. Horizontal layout with image on the left,
|
||||||
|
* name + meta on the right.
|
||||||
|
*
|
||||||
|
* For the full vertical listing card, use ProviderCard instead.
|
||||||
|
*
|
||||||
|
* Composes Card + Typography.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```tsx
|
||||||
|
* <ProviderCardCompact
|
||||||
|
* name="H.Parsons"
|
||||||
|
* location="Wentworth"
|
||||||
|
* imageUrl="/images/parsons.jpg"
|
||||||
|
* rating={4.5}
|
||||||
|
* reviewCount={11}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const ProviderCardCompact = React.forwardRef<HTMLDivElement, ProviderCardCompactProps>(
|
||||||
|
({ name, location, imageUrl, rating, reviewCount, onClick, sx }, ref) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
ref={ref}
|
||||||
|
variant="outlined"
|
||||||
|
interactive={!!onClick}
|
||||||
|
padding="none"
|
||||||
|
onClick={onClick}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
minHeight: 110,
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Image */}
|
||||||
|
{imageUrl && (
|
||||||
|
<Box
|
||||||
|
role="img"
|
||||||
|
aria-label={`${name} photo`}
|
||||||
|
sx={{
|
||||||
|
width: { xs: 120, sm: 160 },
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundImage: `url(${imageUrl})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
p: 2,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" component="span">
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<LocationOnOutlinedIcon
|
||||||
|
sx={{ fontSize: 16, color: 'text.secondary' }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{location}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Rating */}
|
||||||
|
{rating != null && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<StarRoundedIcon
|
||||||
|
sx={{ fontSize: 16, color: 'var(--fa-color-brand-500)' }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{rating} Rating{reviewCount != null ? ` (${reviewCount} ${reviewCount === 1 ? 'Review' : 'Reviews'})` : ''}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ProviderCardCompact.displayName = 'ProviderCardCompact';
|
||||||
|
export default ProviderCardCompact;
|
||||||
1
src/components/molecules/ProviderCardCompact/index.ts
Normal file
1
src/components/molecules/ProviderCardCompact/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ProviderCardCompact, type ProviderCardCompactProps } from './ProviderCardCompact';
|
||||||
228
src/components/organisms/PackageDetail/PackageDetail.stories.tsx
Normal file
228
src/components/organisms/PackageDetail/PackageDetail.stories.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { PackageDetail } from './PackageDetail';
|
||||||
|
import { ServiceOption } from '../../molecules/ServiceOption';
|
||||||
|
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
|
||||||
|
import { Chip } from '../../atoms/Chip';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Navigation } from '../Navigation';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
|
||||||
|
const DEMO_IMAGE = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
|
||||||
|
|
||||||
|
const essentials = [
|
||||||
|
{ name: 'Accommodation', price: 1500, info: 'Refrigerated holding of the deceased prior to the funeral service.' },
|
||||||
|
{ name: 'Death Registration Certificate', price: 1500, info: 'Lodgement of death registration with NSW Registry of Births, Deaths & Marriages.' },
|
||||||
|
{ name: 'Doctor Fee for Cremation', price: 1500, info: 'Statutory medical referee fee required for all cremations in NSW.' },
|
||||||
|
{ name: 'NSW Government Levy — Cremation', price: 1500, info: 'NSW Government cremation levy as set by the Department of Health.' },
|
||||||
|
{ name: 'Professional Mortuary Care', price: 1500, info: 'Preparation and care of the deceased.' },
|
||||||
|
{ name: 'Professional Service Fee', price: 1500, info: 'Coordination of all funeral arrangements and services.' },
|
||||||
|
{ name: 'Allowance for Coffin', price: 1500, isAllowance: true, info: 'Allowance amount — upgrade options available during arrangement.' },
|
||||||
|
{ name: 'Allowance for Crematorium', price: 1500, isAllowance: true, info: 'Allowance for crematorium fees — varies by location.' },
|
||||||
|
{ name: 'Allowance for Hearse', price: 1500, isAllowance: true, info: 'Allowance for hearse transfer — distance surcharges may apply.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const complimentary = [
|
||||||
|
{ name: 'Dressing Fee', info: 'Dressing and preparation of the deceased — included at no charge.' },
|
||||||
|
{ name: 'Viewing Fee', info: 'One private family viewing — included at no charge.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const extras = [
|
||||||
|
{ name: 'Allowance for Flowers', price: 1500, isAllowance: true, info: 'Seasonal floral arrangements for the service.' },
|
||||||
|
{ name: 'Allowance for Master of Ceremonies', price: 1500, isAllowance: true, info: 'Professional celebrant or MC for the funeral service.' },
|
||||||
|
{ name: 'After Business Hours Service Surcharge', price: 1500, info: 'Additional fee for services held outside standard business hours.' },
|
||||||
|
{ name: 'After Hours Prayers', price: 1500, info: 'Evening prayer service at the funeral home.' },
|
||||||
|
{ name: 'Coffin Bearing by Funeral Directors', price: 1500, info: 'Professional pallbearing by funeral directors.' },
|
||||||
|
{ name: 'Digital Recording', price: 1500, info: 'Professional video recording of the funeral service.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const termsText = '* This package includes a funeral service at a chapel or a church with a funeral procession following to the crematorium. It includes many of the most commonly selected funeral options preselected for you. Many people choose this package for the extended funeral rituals — of course, you can tailor the funeral service to meet your needs and budget as you go through the selections.';
|
||||||
|
|
||||||
|
const packages = [
|
||||||
|
{ id: 'everyday', name: 'Everyday Funeral Package', price: 900, description: 'Our most popular package with all essential services included. Suitable for a traditional chapel or church service.' },
|
||||||
|
{ id: 'deluxe', name: 'Deluxe Funeral Package', price: 1200, description: 'An enhanced package with premium coffin and additional floral arrangements.' },
|
||||||
|
{ id: 'essential', name: 'Essential Funeral Package', price: 600, description: 'A simple, dignified service covering all necessary arrangements.' },
|
||||||
|
{ id: 'catholic', name: 'Catholic Service', price: 950, description: 'A service tailored for Catholic traditions including prayers and church ceremony.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const funeralTypes = ['All', 'Cremation', 'Burial', 'Memorial', 'Catholic', 'Direct Cremation'];
|
||||||
|
|
||||||
|
const FALogoNav = () => (
|
||||||
|
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 28 }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const meta: Meta<typeof PackageDetail> = {
|
||||||
|
title: 'Organisms/PackageDetail',
|
||||||
|
component: PackageDetail,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ maxWidth: 600, width: '100%' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof PackageDetail>;
|
||||||
|
|
||||||
|
// --- Default -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Full package detail panel with all sections */
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Everyday Funeral Package',
|
||||||
|
price: 900,
|
||||||
|
sections: [
|
||||||
|
{ heading: 'Essentials', items: essentials },
|
||||||
|
{ heading: 'Complimentary Items', items: complimentary },
|
||||||
|
{ heading: 'Extras', items: extras },
|
||||||
|
],
|
||||||
|
total: 2700,
|
||||||
|
terms: termsText,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Without Extras ----------------------------------------------------------
|
||||||
|
|
||||||
|
/** Simpler package with essentials only */
|
||||||
|
export const WithoutExtras: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Essential Funeral Package',
|
||||||
|
price: 600,
|
||||||
|
sections: [
|
||||||
|
{ heading: 'Essentials', items: essentials.slice(0, 6) },
|
||||||
|
{ heading: 'Complimentary Items', items: complimentary },
|
||||||
|
],
|
||||||
|
total: 9000,
|
||||||
|
terms: termsText,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Package Select Page Layout ----------------------------------------------
|
||||||
|
|
||||||
|
/** Full page layout — left: package list, right: detail panel */
|
||||||
|
export const PackageSelectPage: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ maxWidth: 'none', width: '100%' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => {
|
||||||
|
const [selectedPkg, setSelectedPkg] = useState('everyday');
|
||||||
|
const [activeFilter, setActiveFilter] = useState('Cremation');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Navigation
|
||||||
|
logo={<FALogoNav />}
|
||||||
|
items={[
|
||||||
|
{ label: 'Provider Portal', href: '/provider-portal' },
|
||||||
|
{ label: 'FAQ', href: '/faq' },
|
||||||
|
{ label: 'Contact Us', href: '/contact' },
|
||||||
|
{ label: 'Log in', href: '/login' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
|
||||||
|
gap: { xs: 3, md: 4 },
|
||||||
|
maxWidth: 'lg',
|
||||||
|
mx: 'auto',
|
||||||
|
px: { xs: 2, md: 4 },
|
||||||
|
py: { xs: 2, md: 4 },
|
||||||
|
alignItems: 'start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Left column: back, heading, provider, filter, packages */}
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
sx={{ mb: 2, ml: -1 }}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Typography variant="h2" sx={{ mb: 3 }}>
|
||||||
|
Select a package
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ProviderCardCompact
|
||||||
|
name="H.Parsons"
|
||||||
|
location="Wentworth"
|
||||||
|
imageUrl={DEMO_IMAGE}
|
||||||
|
rating={4.5}
|
||||||
|
reviewCount={11}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Funeral type filter */}
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
|
||||||
|
{funeralTypes.map((type) => (
|
||||||
|
<Chip
|
||||||
|
key={type}
|
||||||
|
label={type}
|
||||||
|
variant={activeFilter === type ? 'filled' : 'outlined'}
|
||||||
|
selected={activeFilter === type}
|
||||||
|
onClick={() => setActiveFilter(type)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h4" sx={{ mb: 2 }}>
|
||||||
|
Packages
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="Available packages"
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
|
||||||
|
>
|
||||||
|
{packages.map((pkg) => (
|
||||||
|
<ServiceOption
|
||||||
|
key={pkg.id}
|
||||||
|
name={pkg.name}
|
||||||
|
price={pkg.price}
|
||||||
|
description={pkg.description}
|
||||||
|
selected={selectedPkg === pkg.id}
|
||||||
|
onClick={() => setSelectedPkg(pkg.id)}
|
||||||
|
maxDescriptionLines={2}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Right column: package detail */}
|
||||||
|
<Box sx={{ position: { md: 'sticky' }, top: { md: 96 } }}>
|
||||||
|
<PackageDetail
|
||||||
|
name={packages.find((p) => p.id === selectedPkg)?.name ?? ''}
|
||||||
|
price={packages.find((p) => p.id === selectedPkg)?.price ?? 0}
|
||||||
|
sections={[
|
||||||
|
{ heading: 'Essentials', items: essentials },
|
||||||
|
{ heading: 'Complimentary Items', items: complimentary },
|
||||||
|
{ heading: 'Extras', items: extras },
|
||||||
|
]}
|
||||||
|
total={2700}
|
||||||
|
terms={termsText}
|
||||||
|
onArrange={() => alert(`Making arrangement for: ${selectedPkg}`)}
|
||||||
|
onCompare={() => alert(`Added ${selectedPkg} to compare`)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
196
src/components/organisms/PackageDetail/PackageDetail.tsx
Normal file
196
src/components/organisms/PackageDetail/PackageDetail.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
import { LineItem } from '../../molecules/LineItem';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** A single item within a package section */
|
||||||
|
export interface PackageLineItem {
|
||||||
|
/** Item name */
|
||||||
|
name: string;
|
||||||
|
/** Tooltip description — clients have laboured over these, display them all */
|
||||||
|
info?: string;
|
||||||
|
/** Price in dollars — omit for complimentary items */
|
||||||
|
price?: number;
|
||||||
|
/** Whether this is an allowance (shows asterisk) */
|
||||||
|
isAllowance?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A section of items within a package (e.g. "Essentials", "Extras") */
|
||||||
|
export interface PackageSection {
|
||||||
|
/** Section heading */
|
||||||
|
heading: string;
|
||||||
|
/** Items in this section */
|
||||||
|
items: PackageLineItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Props for the FA PackageDetail organism */
|
||||||
|
export interface PackageDetailProps {
|
||||||
|
/** Package name */
|
||||||
|
name: string;
|
||||||
|
/** Package price in dollars */
|
||||||
|
price: number;
|
||||||
|
/** Grouped sections of package contents */
|
||||||
|
sections: PackageSection[];
|
||||||
|
/** Package total — usually the sum of priced essentials */
|
||||||
|
total?: number;
|
||||||
|
/** Terms and conditions text — required by providers */
|
||||||
|
terms?: string;
|
||||||
|
/** Called when user clicks "Make Arrangement" */
|
||||||
|
onArrange?: () => void;
|
||||||
|
/** Called when user clicks "Compare" */
|
||||||
|
onCompare?: () => void;
|
||||||
|
/** Whether the arrange button is disabled */
|
||||||
|
arrangeDisabled?: boolean;
|
||||||
|
/** MUI sx prop for the root element */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Package detail panel for the FA design system.
|
||||||
|
*
|
||||||
|
* Displays the full contents of a funeral package — name, price, CTA buttons,
|
||||||
|
* grouped line items (Essentials, Complimentary, Extras), total, and T&Cs.
|
||||||
|
*
|
||||||
|
* Used as the right-side panel on the Package Select page. The contents and
|
||||||
|
* T&Cs are provider-authored and must be displayed in full.
|
||||||
|
*
|
||||||
|
* "Make Arrangement" is the FA term for selecting/committing to a package.
|
||||||
|
*
|
||||||
|
* Composes Typography + Button + Divider + LineItem.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```tsx
|
||||||
|
* <PackageDetail
|
||||||
|
* name="Everyday Funeral Package"
|
||||||
|
* price={2700}
|
||||||
|
* sections={[
|
||||||
|
* { heading: 'Essentials', items: [...] },
|
||||||
|
* { heading: 'Complimentary Items', items: [...] },
|
||||||
|
* ]}
|
||||||
|
* total={2700}
|
||||||
|
* terms="* This package includes a funeral service at a chapel..."
|
||||||
|
* onArrange={() => startArrangement(pkg.id)}
|
||||||
|
* onCompare={() => addToCompare(pkg.id)}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
sections,
|
||||||
|
total,
|
||||||
|
terms,
|
||||||
|
onArrange,
|
||||||
|
onCompare,
|
||||||
|
arrangeDisabled = false,
|
||||||
|
sx,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 'var(--fa-card-border-radius-default)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Main content area */}
|
||||||
|
<Box sx={{ px: { xs: 2, sm: 3 }, py: 2 }}>
|
||||||
|
{/* Header: name + price */}
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="h4" component="h2">
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
${price.toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* CTA buttons */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 1.5, mb: 3 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
disabled={arrangeDisabled}
|
||||||
|
onClick={onArrange}
|
||||||
|
>
|
||||||
|
Make Arrangement
|
||||||
|
</Button>
|
||||||
|
{onCompare && (
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
onClick={onCompare}
|
||||||
|
sx={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
Compare
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
{/* Sections */}
|
||||||
|
{sections.map((section, sectionIdx) => (
|
||||||
|
<Box key={section.heading} sx={{ mb: sectionIdx < sections.length - 1 ? 2 : 0 }}>
|
||||||
|
<Typography variant="label" sx={{ display: 'block', mb: 1.5 }}>
|
||||||
|
{section.heading}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<LineItem
|
||||||
|
key={item.name}
|
||||||
|
name={item.name}
|
||||||
|
info={item.info}
|
||||||
|
price={item.price}
|
||||||
|
isAllowance={item.isAllowance}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
{total != null && (
|
||||||
|
<LineItem name="Total" price={total} variant="total" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Terms & Conditions footer */}
|
||||||
|
{terms && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||||
|
px: { xs: 2, sm: 3 },
|
||||||
|
py: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="captionSm" color="text.secondary" sx={{ lineHeight: 1.5 }}>
|
||||||
|
{terms}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
PackageDetail.displayName = 'PackageDetail';
|
||||||
|
export default PackageDetail;
|
||||||
1
src/components/organisms/PackageDetail/index.ts
Normal file
1
src/components/organisms/PackageDetail/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { PackageDetail, type PackageDetailProps, type PackageSection, type PackageLineItem } from './PackageDetail';
|
||||||
Reference in New Issue
Block a user