Compare commits
2 Commits
ed78fc6cc2
...
593cd82122
| Author | SHA1 | Date | |
|---|---|---|---|
| 593cd82122 | |||
| b7a2a4e136 |
@@ -0,0 +1,163 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { ComparisonPackageCard } from './ComparisonPackageCard';
|
||||
import type { ComparisonPackage } from '../../organisms/ComparisonTable';
|
||||
|
||||
// ─── Mock data ──────────────────────────────────────────────────────────────
|
||||
|
||||
const basePackage: ComparisonPackage = {
|
||||
id: 'wollongong-everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 6966,
|
||||
provider: {
|
||||
name: 'Wollongong City Funerals',
|
||||
location: 'Wollongong',
|
||||
rating: 4.8,
|
||||
reviewCount: 122,
|
||||
verified: true,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
heading: 'Essentials',
|
||||
items: [
|
||||
{
|
||||
name: 'Allowance for Coffin',
|
||||
info: 'Allowance amount — upgrade options available.',
|
||||
value: { type: 'allowance', amount: 1750 },
|
||||
},
|
||||
{
|
||||
name: 'Cremation Certificate/Permit',
|
||||
info: 'Statutory medical referee fee.',
|
||||
value: { type: 'price', amount: 350 },
|
||||
},
|
||||
{
|
||||
name: 'Crematorium',
|
||||
info: 'Cremation facility fees.',
|
||||
value: { type: 'price', amount: 660 },
|
||||
},
|
||||
{
|
||||
name: 'Professional Service Fee',
|
||||
info: 'Coordination of arrangements.',
|
||||
value: { type: 'price', amount: 3650.9 },
|
||||
},
|
||||
{
|
||||
name: 'Transportation Service Fee',
|
||||
info: 'Transfer of the deceased.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Optionals',
|
||||
items: [
|
||||
{
|
||||
name: 'Digital Recording',
|
||||
info: 'Professional video recording.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
{ name: 'Online Notice', info: 'Online death notice.', value: { type: 'complimentary' } },
|
||||
{ name: 'Viewing Fee', info: 'One private family viewing.', value: { type: 'included' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Extras',
|
||||
items: [
|
||||
{
|
||||
name: 'Allowance for Celebrant',
|
||||
info: 'Professional celebrant or MC.',
|
||||
value: { type: 'allowance', amount: 550 },
|
||||
},
|
||||
{ name: 'Catering', info: 'Post-service catering.', value: { type: 'poa' } },
|
||||
{
|
||||
name: 'Saturday Service Fee',
|
||||
info: 'Additional fee for Saturday services.',
|
||||
value: { type: 'price', amount: 880 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const unverifiedPackage: ComparisonPackage = {
|
||||
...basePackage,
|
||||
id: 'inglewood-everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 7200,
|
||||
provider: {
|
||||
name: 'Inglewood Chapel',
|
||||
location: 'Inglewood',
|
||||
rating: 4.2,
|
||||
reviewCount: 45,
|
||||
verified: false,
|
||||
},
|
||||
};
|
||||
|
||||
const recommendedPackage: ComparisonPackage = {
|
||||
...basePackage,
|
||||
id: 'recommended-premium',
|
||||
name: 'Premium Cremation Service',
|
||||
price: 8450,
|
||||
provider: {
|
||||
name: 'H. Parsons Funeral Directors',
|
||||
location: 'Wentworth',
|
||||
rating: 4.9,
|
||||
reviewCount: 203,
|
||||
verified: true,
|
||||
},
|
||||
isRecommended: true,
|
||||
};
|
||||
|
||||
// ─── Meta ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const meta: Meta<typeof ComparisonPackageCard> = {
|
||||
title: 'Molecules/ComparisonPackageCard',
|
||||
component: ComparisonPackageCard,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ maxWidth: 400, mx: 'auto' }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
args: {
|
||||
onArrange: (id) => alert(`Arrange: ${id}`),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ComparisonPackageCard>;
|
||||
|
||||
/** Verified provider — default appearance used in ComparisonPage mobile tab panel */
|
||||
export const Verified: Story = {
|
||||
args: {
|
||||
pkg: basePackage,
|
||||
},
|
||||
};
|
||||
|
||||
/** Unverified provider — "Make Enquiry" CTA + soft button variant, no verified badge */
|
||||
export const Unverified: Story = {
|
||||
args: {
|
||||
pkg: unverifiedPackage,
|
||||
},
|
||||
};
|
||||
|
||||
/** Recommended package — warm banner, selected card state, warm header background */
|
||||
export const Recommended: Story = {
|
||||
args: {
|
||||
pkg: recommendedPackage,
|
||||
},
|
||||
};
|
||||
|
||||
/** Itemisation unavailable — used when a provider hasn't submitted an itemised breakdown */
|
||||
export const ItemizedUnavailable: Story = {
|
||||
args: {
|
||||
pkg: {
|
||||
...unverifiedPackage,
|
||||
itemizedAvailable: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,290 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
import { Badge } from '../../atoms/Badge';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import type { ComparisonPackage, ComparisonCellValue } from '../../organisms/ComparisonTable';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ComparisonPackageCardProps {
|
||||
/** Package data to render — same shape used by ComparisonTable */
|
||||
pkg: ComparisonPackage;
|
||||
/** Called when the user clicks the CTA (Make Arrangement / Make Enquiry) */
|
||||
onArrange: (packageId: string) => void;
|
||||
/** MUI sx prop for container overrides */
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatPrice(amount: number): string {
|
||||
return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`;
|
||||
}
|
||||
|
||||
function CellValue({ value }: { value: ComparisonCellValue }) {
|
||||
switch (value.type) {
|
||||
case 'price':
|
||||
return (
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right' }}>
|
||||
{formatPrice(value.amount)}
|
||||
</Typography>
|
||||
);
|
||||
case 'allowance':
|
||||
return (
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right' }}>
|
||||
{formatPrice(value.amount)}*
|
||||
</Typography>
|
||||
);
|
||||
case 'complimentary':
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, justifyContent: 'flex-end' }}>
|
||||
<CheckCircleOutlineIcon
|
||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: 'var(--fa-color-feedback-success)' }}>
|
||||
Complimentary
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 'included':
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, justifyContent: 'flex-end' }}>
|
||||
<CheckCircleOutlineIcon
|
||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: 'var(--fa-color-feedback-success)' }}>
|
||||
Included
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 'poa':
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: 'italic', textAlign: 'right' }}
|
||||
>
|
||||
Price On Application
|
||||
</Typography>
|
||||
);
|
||||
case 'unknown':
|
||||
return (
|
||||
<Badge color="default" variant="soft" size="small">
|
||||
Unknown
|
||||
</Badge>
|
||||
);
|
||||
case 'unavailable':
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'var(--fa-color-neutral-400)', textAlign: 'right' }}
|
||||
>
|
||||
—
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Component ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Mobile package card for the ComparisonPage mobile tab panel view.
|
||||
*
|
||||
* Full-width card with provider header (verified badge, name, location, rating,
|
||||
* package name, price, CTA) and the package's itemised sections below. Used as
|
||||
* the content of each mobile tabpanel — one card visible at a time, selected
|
||||
* via the tab rail.
|
||||
*
|
||||
* Shared by ComparisonPage (V2) and ComparisonPageV1 so that card-level tweaks
|
||||
* land in a single file.
|
||||
*/
|
||||
export const ComparisonPackageCard = React.forwardRef<HTMLDivElement, ComparisonPackageCardProps>(
|
||||
({ pkg, onArrange, sx }, ref) => {
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
variant="outlined"
|
||||
selected={pkg.isRecommended}
|
||||
padding="none"
|
||||
sx={[{ overflow: 'hidden' }, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||
>
|
||||
{/* Recommended banner */}
|
||||
{pkg.isRecommended && (
|
||||
<Box sx={{ bgcolor: 'var(--fa-color-brand-600)', py: 0.75, textAlign: 'center' }}>
|
||||
<Typography
|
||||
variant="labelSm"
|
||||
sx={{
|
||||
color: 'var(--fa-color-white)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Recommended
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Provider header */}
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: pkg.isRecommended
|
||||
? 'var(--fa-color-surface-warm)'
|
||||
: 'var(--fa-color-surface-subtle)',
|
||||
px: 2.5,
|
||||
pt: 2.5,
|
||||
pb: 2,
|
||||
}}
|
||||
>
|
||||
{/* Verified badge */}
|
||||
{pkg.provider.verified && (
|
||||
<Badge
|
||||
color="brand"
|
||||
variant="soft"
|
||||
size="small"
|
||||
icon={<VerifiedOutlinedIcon sx={{ fontSize: 14 }} />}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* Provider name */}
|
||||
<Typography variant="label" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
|
||||
{pkg.provider.name}
|
||||
</Typography>
|
||||
|
||||
{/* Location + Rating */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<LocationOnOutlinedIcon sx={{ fontSize: 14, color: 'text.secondary' }} aria-hidden />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.provider.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
{pkg.provider.rating != null && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<StarRoundedIcon
|
||||
sx={{ fontSize: 14, color: 'var(--fa-color-brand-500)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.provider.rating}
|
||||
{pkg.provider.reviewCount != null && ` (${pkg.provider.reviewCount})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 1.5 }} />
|
||||
|
||||
{/* Package name + price */}
|
||||
<Typography variant="h5" component="p">
|
||||
{pkg.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Total package price
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ color: 'primary.main', fontWeight: 700 }}>
|
||||
{formatPrice(pkg.price)}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant={pkg.provider.verified ? 'contained' : 'soft'}
|
||||
color={pkg.provider.verified ? 'primary' : 'secondary'}
|
||||
size="large"
|
||||
fullWidth
|
||||
onClick={() => onArrange(pkg.id)}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{pkg.provider.verified ? 'Make Arrangement' : 'Make Enquiry'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Sections — with left accent borders on headings */}
|
||||
<Box sx={{ px: 2.5, py: 2.5 }}>
|
||||
{pkg.itemizedAvailable === false ? (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
||||
Itemised pricing not available for this provider.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
pkg.sections.map((section, sIdx) => (
|
||||
<Box key={section.heading} sx={{ mb: sIdx < pkg.sections.length - 1 ? 3 : 0 }}>
|
||||
{/* Section heading with left accent */}
|
||||
<Box
|
||||
sx={{
|
||||
borderLeft: '3px solid',
|
||||
borderLeftColor: 'var(--fa-color-brand-500)',
|
||||
pl: 1.5,
|
||||
mb: 1.5,
|
||||
mt: sIdx > 0 ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="h3">
|
||||
{section.heading}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{section.items.map((item) => (
|
||||
<Box
|
||||
key={item.name}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
py: 1.5,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ minWidth: 0, flex: '1 1 50%', maxWidth: '60%' }}>
|
||||
<Typography variant="body2" color="text.secondary" component="span">
|
||||
{item.name}
|
||||
</Typography>
|
||||
{item.info && (
|
||||
<Box component="span" sx={{ whiteSpace: 'nowrap' }}>
|
||||
{'\u00A0'}
|
||||
<Tooltip title={item.info} arrow placement="top">
|
||||
<InfoOutlinedIcon
|
||||
aria-label={`More information about ${item.name}`}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
color: 'var(--fa-color-neutral-400)',
|
||||
cursor: 'help',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<CellValue value={item.value} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ComparisonPackageCard.displayName = 'ComparisonPackageCard';
|
||||
export default ComparisonPackageCard;
|
||||
2
src/components/molecules/ComparisonPackageCard/index.ts
Normal file
2
src/components/molecules/ComparisonPackageCard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ComparisonPackageCard, default } from './ComparisonPackageCard';
|
||||
export type { ComparisonPackageCardProps } from './ComparisonPackageCard';
|
||||
@@ -2,26 +2,16 @@ import React, { useId, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
|
||||
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
import { Badge } from '../../atoms/Badge';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { WizardLayout } from '../../templates/WizardLayout';
|
||||
import {
|
||||
ComparisonTable,
|
||||
type ComparisonPackage,
|
||||
type ComparisonCellValue,
|
||||
} from '../../organisms/ComparisonTable';
|
||||
import { ComparisonTable, type ComparisonPackage } from '../../organisms/ComparisonTable';
|
||||
import { ComparisonPackageCard } from '../../molecules/ComparisonPackageCard';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -47,259 +37,6 @@ export interface ComparisonPageProps {
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatPrice(amount: number): string {
|
||||
return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`;
|
||||
}
|
||||
|
||||
function MobileCellValue({ value }: { value: ComparisonCellValue }) {
|
||||
switch (value.type) {
|
||||
case 'price':
|
||||
return (
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right' }}>
|
||||
{formatPrice(value.amount)}
|
||||
</Typography>
|
||||
);
|
||||
case 'allowance':
|
||||
return (
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right' }}>
|
||||
{formatPrice(value.amount)}*
|
||||
</Typography>
|
||||
);
|
||||
case 'complimentary':
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, justifyContent: 'flex-end' }}>
|
||||
<CheckCircleOutlineIcon
|
||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: 'var(--fa-color-feedback-success)' }}>
|
||||
Complimentary
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 'included':
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, justifyContent: 'flex-end' }}>
|
||||
<CheckCircleOutlineIcon
|
||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: 'var(--fa-color-feedback-success)' }}>
|
||||
Included
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 'poa':
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: 'italic', textAlign: 'right' }}
|
||||
>
|
||||
Price On Application
|
||||
</Typography>
|
||||
);
|
||||
case 'unknown':
|
||||
return (
|
||||
<Badge color="default" variant="soft" size="small">
|
||||
Unknown
|
||||
</Badge>
|
||||
);
|
||||
case 'unavailable':
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'var(--fa-color-neutral-400)', textAlign: 'right' }}
|
||||
>
|
||||
—
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Mobile card view ───────────────────────────────────────────────────────
|
||||
|
||||
function MobilePackageCard({
|
||||
pkg,
|
||||
onArrange,
|
||||
}: {
|
||||
pkg: ComparisonPackage;
|
||||
onArrange: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
selected={pkg.isRecommended}
|
||||
padding="none"
|
||||
sx={{ overflow: 'hidden' }}
|
||||
>
|
||||
{/* Recommended banner */}
|
||||
{pkg.isRecommended && (
|
||||
<Box sx={{ bgcolor: 'var(--fa-color-brand-600)', py: 0.75, textAlign: 'center' }}>
|
||||
<Typography
|
||||
variant="labelSm"
|
||||
sx={{
|
||||
color: 'var(--fa-color-white)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Recommended
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Provider header */}
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: pkg.isRecommended
|
||||
? 'var(--fa-color-surface-warm)'
|
||||
: 'var(--fa-color-surface-subtle)',
|
||||
px: 2.5,
|
||||
pt: 2.5,
|
||||
pb: 2,
|
||||
}}
|
||||
>
|
||||
{/* Verified badge */}
|
||||
{pkg.provider.verified && (
|
||||
<Badge
|
||||
color="brand"
|
||||
variant="soft"
|
||||
size="small"
|
||||
icon={<VerifiedOutlinedIcon sx={{ fontSize: 14 }} />}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* Provider name */}
|
||||
<Typography variant="label" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
|
||||
{pkg.provider.name}
|
||||
</Typography>
|
||||
|
||||
{/* Location + Rating */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<LocationOnOutlinedIcon sx={{ fontSize: 14, color: 'text.secondary' }} aria-hidden />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.provider.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
{pkg.provider.rating != null && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<StarRoundedIcon
|
||||
sx={{ fontSize: 14, color: 'var(--fa-color-brand-500)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.provider.rating}
|
||||
{pkg.provider.reviewCount != null && ` (${pkg.provider.reviewCount})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 1.5 }} />
|
||||
|
||||
{/* Package name + price */}
|
||||
<Typography variant="h5" component="p">
|
||||
{pkg.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Total package price
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ color: 'primary.main', fontWeight: 700 }}>
|
||||
{formatPrice(pkg.price)}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant={pkg.provider.verified ? 'contained' : 'soft'}
|
||||
color={pkg.provider.verified ? 'primary' : 'secondary'}
|
||||
size="large"
|
||||
fullWidth
|
||||
onClick={() => onArrange(pkg.id)}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{pkg.provider.verified ? 'Make Arrangement' : 'Make Enquiry'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Sections — with left accent borders on headings */}
|
||||
<Box sx={{ px: 2.5, py: 2.5 }}>
|
||||
{pkg.itemizedAvailable === false ? (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
||||
Itemised pricing not available for this provider.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
pkg.sections.map((section, sIdx) => (
|
||||
<Box key={section.heading} sx={{ mb: sIdx < pkg.sections.length - 1 ? 3 : 0 }}>
|
||||
{/* Section heading with left accent */}
|
||||
<Box
|
||||
sx={{
|
||||
borderLeft: '3px solid',
|
||||
borderLeftColor: 'var(--fa-color-brand-500)',
|
||||
pl: 1.5,
|
||||
mb: 1.5,
|
||||
mt: sIdx > 0 ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="h3">
|
||||
{section.heading}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{section.items.map((item) => (
|
||||
<Box
|
||||
key={item.name}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
py: 1.5,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ minWidth: 0, flex: '1 1 50%', maxWidth: '60%' }}>
|
||||
<Typography variant="body2" color="text.secondary" component="span">
|
||||
{item.name}
|
||||
</Typography>
|
||||
{item.info && (
|
||||
<Box component="span" sx={{ whiteSpace: 'nowrap' }}>
|
||||
{'\u00A0'}
|
||||
<Tooltip title={item.info} arrow placement="top">
|
||||
<InfoOutlinedIcon
|
||||
aria-label={`More information about ${item.name}`}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
color: 'var(--fa-color-neutral-400)',
|
||||
cursor: 'help',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<MobileCellValue value={item.value} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Component ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -511,7 +248,7 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
||||
id={`comparison-tabpanel-${activeTabIdx}`}
|
||||
aria-labelledby={`comparison-tab-${activeTabIdx}`}
|
||||
>
|
||||
<MobilePackageCard pkg={activePackage} onArrange={onArrange} />
|
||||
<ComparisonPackageCard pkg={activePackage} onArrange={onArrange} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,26 +2,16 @@ import React, { useId, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
|
||||
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
import { Badge } from '../../atoms/Badge';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { WizardLayout } from '../../templates/WizardLayout';
|
||||
import {
|
||||
ComparisonTable,
|
||||
type ComparisonPackage,
|
||||
type ComparisonCellValue,
|
||||
} from '../../organisms/ComparisonTable';
|
||||
import { ComparisonTable, type ComparisonPackage } from '../../organisms/ComparisonTable';
|
||||
import { ComparisonPackageCard } from '../../molecules/ComparisonPackageCard';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -47,259 +37,6 @@ export interface ComparisonPageV1Props {
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatPrice(amount: number): string {
|
||||
return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`;
|
||||
}
|
||||
|
||||
function MobileCellValue({ value }: { value: ComparisonCellValue }) {
|
||||
switch (value.type) {
|
||||
case 'price':
|
||||
return (
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right' }}>
|
||||
{formatPrice(value.amount)}
|
||||
</Typography>
|
||||
);
|
||||
case 'allowance':
|
||||
return (
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right' }}>
|
||||
{formatPrice(value.amount)}*
|
||||
</Typography>
|
||||
);
|
||||
case 'complimentary':
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, justifyContent: 'flex-end' }}>
|
||||
<CheckCircleOutlineIcon
|
||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: 'var(--fa-color-feedback-success)' }}>
|
||||
Complimentary
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 'included':
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, justifyContent: 'flex-end' }}>
|
||||
<CheckCircleOutlineIcon
|
||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: 'var(--fa-color-feedback-success)' }}>
|
||||
Included
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 'poa':
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: 'italic', textAlign: 'right' }}
|
||||
>
|
||||
Price On Application
|
||||
</Typography>
|
||||
);
|
||||
case 'unknown':
|
||||
return (
|
||||
<Badge color="default" variant="soft" size="small">
|
||||
Unknown
|
||||
</Badge>
|
||||
);
|
||||
case 'unavailable':
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'var(--fa-color-neutral-400)', textAlign: 'right' }}
|
||||
>
|
||||
—
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Mobile card view ───────────────────────────────────────────────────────
|
||||
|
||||
function MobilePackageCard({
|
||||
pkg,
|
||||
onArrange,
|
||||
}: {
|
||||
pkg: ComparisonPackage;
|
||||
onArrange: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
selected={pkg.isRecommended}
|
||||
padding="none"
|
||||
sx={{ overflow: 'hidden' }}
|
||||
>
|
||||
{/* Recommended banner */}
|
||||
{pkg.isRecommended && (
|
||||
<Box sx={{ bgcolor: 'var(--fa-color-brand-600)', py: 0.75, textAlign: 'center' }}>
|
||||
<Typography
|
||||
variant="labelSm"
|
||||
sx={{
|
||||
color: 'var(--fa-color-white)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Recommended
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Provider header */}
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: pkg.isRecommended
|
||||
? 'var(--fa-color-surface-warm)'
|
||||
: 'var(--fa-color-surface-subtle)',
|
||||
px: 2.5,
|
||||
pt: 2.5,
|
||||
pb: 2,
|
||||
}}
|
||||
>
|
||||
{/* Verified badge */}
|
||||
{pkg.provider.verified && (
|
||||
<Badge
|
||||
color="brand"
|
||||
variant="soft"
|
||||
size="small"
|
||||
icon={<VerifiedOutlinedIcon sx={{ fontSize: 14 }} />}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* Provider name */}
|
||||
<Typography variant="label" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
|
||||
{pkg.provider.name}
|
||||
</Typography>
|
||||
|
||||
{/* Location + Rating */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<LocationOnOutlinedIcon sx={{ fontSize: 14, color: 'text.secondary' }} aria-hidden />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.provider.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
{pkg.provider.rating != null && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<StarRoundedIcon
|
||||
sx={{ fontSize: 14, color: 'var(--fa-color-brand-500)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.provider.rating}
|
||||
{pkg.provider.reviewCount != null && ` (${pkg.provider.reviewCount})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 1.5 }} />
|
||||
|
||||
{/* Package name + price */}
|
||||
<Typography variant="h5" component="p">
|
||||
{pkg.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Total package price
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ color: 'primary.main', fontWeight: 700 }}>
|
||||
{formatPrice(pkg.price)}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant={pkg.provider.verified ? 'contained' : 'soft'}
|
||||
color={pkg.provider.verified ? 'primary' : 'secondary'}
|
||||
size="large"
|
||||
fullWidth
|
||||
onClick={() => onArrange(pkg.id)}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{pkg.provider.verified ? 'Make Arrangement' : 'Make Enquiry'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Sections — with left accent borders on headings */}
|
||||
<Box sx={{ px: 2.5, py: 2.5 }}>
|
||||
{pkg.itemizedAvailable === false ? (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
||||
Itemised pricing not available for this provider.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
pkg.sections.map((section, sIdx) => (
|
||||
<Box key={section.heading} sx={{ mb: sIdx < pkg.sections.length - 1 ? 3 : 0 }}>
|
||||
{/* Section heading with left accent */}
|
||||
<Box
|
||||
sx={{
|
||||
borderLeft: '3px solid',
|
||||
borderLeftColor: 'var(--fa-color-brand-500)',
|
||||
pl: 1.5,
|
||||
mb: 1.5,
|
||||
mt: sIdx > 0 ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="h3">
|
||||
{section.heading}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{section.items.map((item) => (
|
||||
<Box
|
||||
key={item.name}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
py: 1.5,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ minWidth: 0, flex: '1 1 50%', maxWidth: '60%' }}>
|
||||
<Typography variant="body2" color="text.secondary" component="span">
|
||||
{item.name}
|
||||
</Typography>
|
||||
{item.info && (
|
||||
<Box component="span" sx={{ whiteSpace: 'nowrap' }}>
|
||||
{'\u00A0'}
|
||||
<Tooltip title={item.info} arrow placement="top">
|
||||
<InfoOutlinedIcon
|
||||
aria-label={`More information about ${item.name}`}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
color: 'var(--fa-color-neutral-400)',
|
||||
cursor: 'help',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<MobileCellValue value={item.value} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Component ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -505,7 +242,7 @@ export const ComparisonPageV1 = React.forwardRef<HTMLDivElement, ComparisonPageV
|
||||
id={`comparison-tabpanel-${activeTabIdx}`}
|
||||
aria-labelledby={`comparison-tab-${activeTabIdx}`}
|
||||
>
|
||||
<MobilePackageCard pkg={activePackage} onArrange={onArrange} />
|
||||
<ComparisonPackageCard pkg={activePackage} onArrange={onArrange} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user