Add package comparison feature: CompareBar, ComparisonTable, ComparisonPage
New components for side-by-side funeral package comparison: - CompareBar molecule: floating bottom pill with fraction badge (1/3, 2/3, 3/3), contextual copy, Compare CTA. For ProvidersStep and PackagesStep. - ComparisonTable organism: CSS Grid comparison with info card, floating verified badges, separate section tables (Essentials/Optionals/Extras) with left accent borders, row hover, horizontal scroll on narrow desktops, font hierarchy. - ComparisonPage: WizardLayout wide-form with Share/Print actions. Desktop shows ComparisonTable, mobile shows mini-card tab rail + single package card view. Recommended package as separate prop (D038). Also fixes PackageDetail: adds priceLabel pass-through (D039), updates stories to Essentials/Optionals/Extras section naming (D035). Decisions: D035-D039 logged. Audits: CompareBar 18/20, ComparisonTable 17/20. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
474
src/components/pages/ComparisonPage/ComparisonPage.stories.tsx
Normal file
474
src/components/pages/ComparisonPage/ComparisonPage.stories.tsx
Normal file
@@ -0,0 +1,474 @@
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { ComparisonPage } from './ComparisonPage';
|
||||
import type { ComparisonPackage } from '../../organisms/ComparisonTable';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
|
||||
const DEMO_LOGO = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=72&h=72&fit=crop';
|
||||
|
||||
const FALogoNav = () => (
|
||||
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 28 }} />
|
||||
);
|
||||
|
||||
// ─── Mock data ──────────────────────────────────────────────────────────────
|
||||
|
||||
const pkgWollongong: ComparisonPackage = {
|
||||
id: 'wollongong-everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 6966,
|
||||
provider: {
|
||||
name: 'Wollongong City Funerals',
|
||||
location: 'Wollongong',
|
||||
logoUrl: DEMO_LOGO,
|
||||
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: 'Death Registration Certificate',
|
||||
info: 'Lodgement with NSW Registry.',
|
||||
value: { type: 'price', amount: 70 },
|
||||
},
|
||||
{
|
||||
name: 'Dressing Fee',
|
||||
info: 'Dressing and preparation.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
{
|
||||
name: 'NSW Government Levy — Cremation',
|
||||
info: 'NSW Government cremation levy.',
|
||||
value: { type: 'price', amount: 45.1 },
|
||||
},
|
||||
{
|
||||
name: 'Professional Mortuary Care',
|
||||
info: 'Preparation and care.',
|
||||
value: { type: 'price', amount: 440 },
|
||||
},
|
||||
{
|
||||
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: 'complimentary' },
|
||||
},
|
||||
{
|
||||
name: 'Flowers',
|
||||
info: 'Seasonal floral arrangements.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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: 'Newspaper Notice', info: 'Published death notice.', value: { type: 'poa' } },
|
||||
{
|
||||
name: 'Saturday Service Fee',
|
||||
info: 'Additional fee for Saturday services.',
|
||||
value: { type: 'price', amount: 880 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const pkgMackay: ComparisonPackage = {
|
||||
id: 'mackay-everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 5495.45,
|
||||
provider: {
|
||||
name: 'Mackay Family Funerals',
|
||||
location: 'Inglewood',
|
||||
logoUrl: DEMO_LOGO,
|
||||
rating: 4.6,
|
||||
reviewCount: 87,
|
||||
verified: true,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
heading: 'Essentials',
|
||||
items: [
|
||||
{
|
||||
name: 'Allowance for Coffin',
|
||||
info: 'Allowance amount.',
|
||||
value: { type: 'allowance', amount: 1500 },
|
||||
},
|
||||
{
|
||||
name: 'Cremation Certificate/Permit',
|
||||
info: 'Medical referee fee.',
|
||||
value: { type: 'price', amount: 350 },
|
||||
},
|
||||
{
|
||||
name: 'Crematorium',
|
||||
info: 'Cremation facility fees.',
|
||||
value: { type: 'price', amount: 660 },
|
||||
},
|
||||
{
|
||||
name: 'Death Registration Certificate',
|
||||
info: 'NSW Registry.',
|
||||
value: { type: 'price', amount: 70 },
|
||||
},
|
||||
{ name: 'Dressing Fee', info: 'Dressing and preparation.', value: { type: 'included' } },
|
||||
{
|
||||
name: 'NSW Government Levy — Cremation',
|
||||
info: 'Government levy.',
|
||||
value: { type: 'price', amount: 45.1 },
|
||||
},
|
||||
{
|
||||
name: 'Professional Mortuary Care',
|
||||
info: 'Preparation and care.',
|
||||
value: { type: 'price', amount: 440 },
|
||||
},
|
||||
{
|
||||
name: 'Professional Service Fee',
|
||||
info: 'Coordination.',
|
||||
value: { type: 'price', amount: 2430.35 },
|
||||
},
|
||||
{ name: 'Transportation Service Fee', info: 'Transfer.', value: { type: 'included' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Optionals',
|
||||
items: [
|
||||
{ name: 'Digital Recording', info: 'Video recording.', value: { type: 'unknown' } },
|
||||
{ name: 'Online Notice', info: 'Online death notice.', value: { type: 'included' } },
|
||||
{ name: 'Viewing Fee', info: 'Private family viewing.', value: { type: 'included' } },
|
||||
{ name: 'Flowers', info: 'Floral arrangements.', value: { type: 'included' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Extras',
|
||||
items: [
|
||||
{
|
||||
name: 'Allowance for Celebrant',
|
||||
info: 'Celebrant or MC.',
|
||||
value: { type: 'allowance', amount: 450 },
|
||||
},
|
||||
{ name: 'Catering', info: 'Post-service catering.', value: { type: 'poa' } },
|
||||
{
|
||||
name: 'Saturday Service Fee',
|
||||
info: 'Saturday surcharge.',
|
||||
value: { type: 'price', amount: 750 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const pkgInglewood: ComparisonPackage = {
|
||||
id: 'inglewood-everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 7200,
|
||||
provider: {
|
||||
name: 'Inglewood Chapel',
|
||||
location: 'Inglewood',
|
||||
logoUrl: DEMO_LOGO,
|
||||
rating: 4.2,
|
||||
reviewCount: 45,
|
||||
verified: false,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
heading: 'Essentials',
|
||||
items: [
|
||||
{
|
||||
name: 'Allowance for Coffin',
|
||||
info: 'Allowance amount.',
|
||||
value: { type: 'allowance', amount: 1800 },
|
||||
},
|
||||
{
|
||||
name: 'Cremation Certificate/Permit',
|
||||
info: 'Medical referee fee.',
|
||||
value: { type: 'price', amount: 350 },
|
||||
},
|
||||
{
|
||||
name: 'Death Registration Certificate',
|
||||
info: 'NSW Registry.',
|
||||
value: { type: 'price', amount: 70 },
|
||||
},
|
||||
{
|
||||
name: 'Professional Service Fee',
|
||||
info: 'Coordination.',
|
||||
value: { type: 'price', amount: 3980 },
|
||||
},
|
||||
{
|
||||
name: 'Transportation Service Fee',
|
||||
info: 'Transfer.',
|
||||
value: { type: 'price', amount: 500 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Optionals',
|
||||
items: [
|
||||
{ name: 'Viewing Fee', info: 'Private family viewing.', value: { type: 'included' } },
|
||||
{ name: 'Flowers', info: 'Floral arrangements.', value: { type: 'poa' } },
|
||||
{
|
||||
name: 'Digital Recording',
|
||||
info: 'Video recording.',
|
||||
value: { type: 'price', amount: 250 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Extras',
|
||||
items: [
|
||||
{ name: 'Catering', info: 'Post-service catering.', value: { type: 'poa' } },
|
||||
{ name: 'Newspaper Notice', info: 'Published death notice.', value: { type: 'poa' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const pkgRecommended: ComparisonPackage = {
|
||||
id: 'recommended-premium',
|
||||
name: 'Premium Cremation Service',
|
||||
price: 8450,
|
||||
provider: {
|
||||
name: 'H. Parsons Funeral Directors',
|
||||
location: 'Wentworth',
|
||||
logoUrl: DEMO_LOGO,
|
||||
rating: 4.9,
|
||||
reviewCount: 203,
|
||||
verified: true,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
heading: 'Essentials',
|
||||
items: [
|
||||
{
|
||||
name: 'Allowance for Coffin',
|
||||
info: 'Premium coffin allowance.',
|
||||
value: { type: 'allowance', amount: 2500 },
|
||||
},
|
||||
{
|
||||
name: 'Cremation Certificate/Permit',
|
||||
info: 'Medical referee fee.',
|
||||
value: { type: 'price', amount: 350 },
|
||||
},
|
||||
{
|
||||
name: 'Crematorium',
|
||||
info: 'Premium crematorium.',
|
||||
value: { type: 'price', amount: 850 },
|
||||
},
|
||||
{
|
||||
name: 'Death Registration Certificate',
|
||||
info: 'NSW Registry.',
|
||||
value: { type: 'price', amount: 70 },
|
||||
},
|
||||
{
|
||||
name: 'Dressing Fee',
|
||||
info: 'Dressing and preparation.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
{
|
||||
name: 'NSW Government Levy — Cremation',
|
||||
info: 'Government levy.',
|
||||
value: { type: 'price', amount: 45.1 },
|
||||
},
|
||||
{
|
||||
name: 'Professional Mortuary Care',
|
||||
info: 'Full preparation and care.',
|
||||
value: { type: 'price', amount: 580 },
|
||||
},
|
||||
{
|
||||
name: 'Professional Service Fee',
|
||||
info: 'Full coordination.',
|
||||
value: { type: 'price', amount: 4054.9 },
|
||||
},
|
||||
{
|
||||
name: 'Transportation Service Fee',
|
||||
info: 'Premium transfer.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Optionals',
|
||||
items: [
|
||||
{
|
||||
name: 'Digital Recording',
|
||||
info: 'HD video recording.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
{ name: 'Online Notice', info: 'Online death notice.', value: { type: 'complimentary' } },
|
||||
{ name: 'Viewing Fee', info: 'Private family viewing.', value: { type: 'complimentary' } },
|
||||
{ name: 'Flowers', info: 'Premium floral arrangements.', value: { type: 'complimentary' } },
|
||||
{ name: 'Webstreaming', info: 'HD live webstream.', value: { type: 'complimentary' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Extras',
|
||||
items: [
|
||||
{
|
||||
name: 'Allowance for Celebrant',
|
||||
info: 'Premium celebrant.',
|
||||
value: { type: 'allowance', amount: 700 },
|
||||
},
|
||||
{
|
||||
name: 'Catering',
|
||||
info: 'Full catering included.',
|
||||
value: { type: 'price', amount: 1200 },
|
||||
},
|
||||
{
|
||||
name: 'Newspaper Notice',
|
||||
info: 'Published death notice.',
|
||||
value: { type: 'price', amount: 350 },
|
||||
},
|
||||
{
|
||||
name: 'Saturday Service Fee',
|
||||
info: 'No Saturday surcharge.',
|
||||
value: { type: 'complimentary' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ─── Meta ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const defaultNav = (
|
||||
<Navigation
|
||||
logo={<FALogoNav />}
|
||||
items={[
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Log in', href: '/login' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const meta: Meta<typeof ComparisonPage> = {
|
||||
title: 'Pages/ComparisonPage',
|
||||
component: ComparisonPage,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {
|
||||
navigation: defaultNav,
|
||||
onShare: () => alert('Share'),
|
||||
onPrint: () => window.print(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ComparisonPage>;
|
||||
|
||||
// --- Default (3 packages, desktop) -------------------------------------------
|
||||
|
||||
/** Three packages from different providers */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
packages: [pkgWollongong, pkgMackay, pkgInglewood],
|
||||
onArrange: (id) => alert(`Arrange: ${id}`),
|
||||
onRemove: (id) => alert(`Remove: ${id}`),
|
||||
onBack: () => alert('Back'),
|
||||
},
|
||||
};
|
||||
|
||||
// --- Two Packages ------------------------------------------------------------
|
||||
|
||||
/** Minimal two-package comparison */
|
||||
export const TwoPackages: Story = {
|
||||
args: {
|
||||
packages: [pkgWollongong, pkgMackay],
|
||||
onArrange: (id) => alert(`Arrange: ${id}`),
|
||||
onRemove: (id) => alert(`Remove: ${id}`),
|
||||
onBack: () => alert('Back'),
|
||||
},
|
||||
};
|
||||
|
||||
// --- With Recommended --------------------------------------------------------
|
||||
|
||||
/** 3 user packages + 1 recommended — recommended shown as additional column/tab */
|
||||
export const WithRecommended: Story = {
|
||||
args: {
|
||||
packages: [pkgWollongong, pkgMackay, pkgInglewood],
|
||||
recommendedPackage: pkgRecommended,
|
||||
onArrange: (id) => alert(`Arrange: ${id}`),
|
||||
onRemove: (id) => alert(`Remove: ${id}`),
|
||||
onBack: () => alert('Back'),
|
||||
},
|
||||
};
|
||||
|
||||
// --- Mobile View -------------------------------------------------------------
|
||||
|
||||
/** Mobile viewport — shows tabbed card view */
|
||||
export const MobileView: Story = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: 'mobile1' },
|
||||
},
|
||||
args: {
|
||||
packages: [pkgWollongong, pkgMackay, pkgInglewood],
|
||||
recommendedPackage: pkgRecommended,
|
||||
onArrange: (id) => alert(`Arrange: ${id}`),
|
||||
onRemove: (id) => alert(`Remove: ${id}`),
|
||||
onBack: () => alert('Back'),
|
||||
},
|
||||
};
|
||||
|
||||
// --- Interactive (with remove) -----------------------------------------------
|
||||
|
||||
/** Interactive — remove packages from comparison */
|
||||
export const Interactive: Story = {
|
||||
render: (args) => {
|
||||
const [pkgs, setPkgs] = useState([pkgWollongong, pkgMackay, pkgInglewood]);
|
||||
|
||||
return (
|
||||
<ComparisonPage
|
||||
{...args}
|
||||
packages={pkgs}
|
||||
recommendedPackage={pkgRecommended}
|
||||
onArrange={(id) => alert(`Make arrangement for: ${id}`)}
|
||||
onRemove={(id) => setPkgs(pkgs.filter((p) => p.id !== id))}
|
||||
onBack={() => alert('Back to packages')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user