Add ComparisonPage V2 with recommended package on the left
Archive the current ComparisonPage as V1 (viewable under Archive/ in Storybook) and build V2 as the new production version. In V2, the recommended package is prepended instead of appended: it appears as the first column on desktop and the first tab in the mobile rail. On mobile the initially active tab is the first user-selected package, not the recommendation — the recommended tab is surfaced as a visible suggestion rather than the default view, which felt too upsell-y for the audience. Both V1 and V2 now use a StarRoundedIcon (brand-600) in the mobile tab label instead of a text star, so the "recommended" marker reads cleanly against both selected and unselected tab backgrounds. See decisions-log D040 for rationale. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -303,13 +303,19 @@ function MobilePackageCard({
|
|||||||
// ─── Component ──────────────────────────────────────────────────────────────
|
// ─── Component ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package comparison page for the FA design system.
|
* Package comparison page for the FA design system (V2 — production).
|
||||||
*
|
*
|
||||||
* Desktop: Full ComparisonTable with info card, floating verified badges,
|
* Desktop: Full ComparisonTable with info card, floating verified badges,
|
||||||
* section tables with left accent borders.
|
* section tables with left accent borders. **Recommended package appears as
|
||||||
* Mobile: Tabbed card view with horizontal chip rail.
|
* the first (leftmost) column.**
|
||||||
|
* Mobile: Tabbed card view with horizontal tab rail. **Recommended package is
|
||||||
|
* the first tab in the rail, but the first user-selected package is the
|
||||||
|
* initially active tab** — the recommended tab is a suggestion, not the
|
||||||
|
* default view.
|
||||||
*
|
*
|
||||||
* Share + Print utility actions in the page header.
|
* Share + Print utility actions in the page header.
|
||||||
|
*
|
||||||
|
* See `ComparisonPageV1.tsx` for the archived V1 (recommended-last) layout.
|
||||||
*/
|
*/
|
||||||
export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPageProps>(
|
export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPageProps>(
|
||||||
(
|
(
|
||||||
@@ -321,14 +327,18 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
|||||||
const tablistId = useId();
|
const tablistId = useId();
|
||||||
|
|
||||||
const allPackages = React.useMemo(() => {
|
const allPackages = React.useMemo(() => {
|
||||||
const result = [...packages];
|
const result: ComparisonPackage[] = [];
|
||||||
if (recommendedPackage) {
|
if (recommendedPackage) {
|
||||||
result.push({ ...recommendedPackage, isRecommended: true });
|
result.push({ ...recommendedPackage, isRecommended: true });
|
||||||
}
|
}
|
||||||
|
result.push(...packages);
|
||||||
return result;
|
return result;
|
||||||
}, [packages, recommendedPackage]);
|
}, [packages, recommendedPackage]);
|
||||||
|
|
||||||
const [activeTabIdx, setActiveTabIdx] = useState(0);
|
// On mobile, default the active tab to the first user-selected package
|
||||||
|
// (not the recommended). Recommended is first in the rail as a suggestion.
|
||||||
|
const defaultTabIdx = recommendedPackage ? 1 : 0;
|
||||||
|
const [activeTabIdx, setActiveTabIdx] = useState(defaultTabIdx);
|
||||||
const activePackage = allPackages[activeTabIdx] ?? allPackages[0];
|
const activePackage = allPackages[activeTabIdx] ?? allPackages[0];
|
||||||
|
|
||||||
const providerCount = new Set(allPackages.map((p) => p.provider.name)).size;
|
const providerCount = new Set(allPackages.map((p) => p.provider.name)).size;
|
||||||
@@ -446,18 +456,37 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ px: 2, py: 1.5 }}>
|
<Box sx={{ px: 2, py: 1.5 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
mb: 0.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pkg.isRecommended && (
|
||||||
|
<StarRoundedIcon
|
||||||
|
aria-label="Recommended"
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: 'var(--fa-color-brand-600)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
variant="labelSm"
|
variant="labelSm"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
display: 'block',
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pkg.isRecommended ? `★ ${pkg.provider.name}` : pkg.provider.name}
|
{pkg.provider.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
|
|||||||
474
src/components/pages/ComparisonPage/ComparisonPageV1.stories.tsx
Normal file
474
src/components/pages/ComparisonPage/ComparisonPageV1.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 { ComparisonPageV1 } from './ComparisonPageV1';
|
||||||
|
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 ComparisonPageV1> = {
|
||||||
|
title: 'Archive/ComparisonPage V1',
|
||||||
|
component: ComparisonPageV1,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
navigation: defaultNav,
|
||||||
|
onShare: () => alert('Share'),
|
||||||
|
onPrint: () => window.print(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ComparisonPageV1>;
|
||||||
|
|
||||||
|
// --- 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 (
|
||||||
|
<ComparisonPageV1
|
||||||
|
{...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')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
520
src/components/pages/ComparisonPage/ComparisonPageV1.tsx
Normal file
520
src/components/pages/ComparisonPage/ComparisonPageV1.tsx
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the ComparisonPageV1 */
|
||||||
|
export interface ComparisonPageV1Props {
|
||||||
|
/** User-selected packages to compare (max 3) */
|
||||||
|
packages: ComparisonPackage[];
|
||||||
|
/** System-recommended package — always shown as an additional column */
|
||||||
|
recommendedPackage?: ComparisonPackage;
|
||||||
|
/** Called when user clicks CTA on a package */
|
||||||
|
onArrange: (packageId: string) => void;
|
||||||
|
/** Called when user removes a package from comparison */
|
||||||
|
onRemove: (packageId: string) => void;
|
||||||
|
/** Called when user clicks Back */
|
||||||
|
onBack: () => void;
|
||||||
|
/** Called when user clicks Share */
|
||||||
|
onShare?: () => void;
|
||||||
|
/** Called when user clicks Print */
|
||||||
|
onPrint?: () => void;
|
||||||
|
/** Navigation bar slot */
|
||||||
|
navigation?: React.ReactNode;
|
||||||
|
/** MUI sx prop */
|
||||||
|
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 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **Archived — V1.** See `ComparisonPage.tsx` (V2) for the production version.
|
||||||
|
*
|
||||||
|
* Package comparison page for the FA design system.
|
||||||
|
*
|
||||||
|
* Desktop: Full ComparisonTable with info card, floating verified badges,
|
||||||
|
* section tables with left accent borders. Recommended package appears as the
|
||||||
|
* **last** column.
|
||||||
|
* Mobile: Tabbed card view with horizontal chip rail. Recommended package is
|
||||||
|
* the last tab.
|
||||||
|
*
|
||||||
|
* Share + Print utility actions in the page header.
|
||||||
|
*/
|
||||||
|
export const ComparisonPageV1 = React.forwardRef<HTMLDivElement, ComparisonPageV1Props>(
|
||||||
|
(
|
||||||
|
{ packages, recommendedPackage, onArrange, onRemove, onBack, onShare, onPrint, navigation, sx },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const tablistId = useId();
|
||||||
|
|
||||||
|
const allPackages = React.useMemo(() => {
|
||||||
|
const result = [...packages];
|
||||||
|
if (recommendedPackage) {
|
||||||
|
result.push({ ...recommendedPackage, isRecommended: true });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [packages, recommendedPackage]);
|
||||||
|
|
||||||
|
const [activeTabIdx, setActiveTabIdx] = useState(0);
|
||||||
|
const activePackage = allPackages[activeTabIdx] ?? allPackages[0];
|
||||||
|
|
||||||
|
const providerCount = new Set(allPackages.map((p) => p.provider.name)).size;
|
||||||
|
const subtitle =
|
||||||
|
providerCount > 1
|
||||||
|
? `Comparing ${packages.length} package${packages.length !== 1 ? 's' : ''} from different providers`
|
||||||
|
: `Comparing ${packages.length} package${packages.length !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={ref} sx={sx}>
|
||||||
|
<WizardLayout
|
||||||
|
variant="wide-form"
|
||||||
|
navigation={navigation}
|
||||||
|
showBackLink
|
||||||
|
backLabel="Back"
|
||||||
|
onBack={onBack}
|
||||||
|
>
|
||||||
|
{/* Page header with Share/Print actions */}
|
||||||
|
<Box sx={{ mb: { xs: 3, md: 5 } }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h2" component="h1" sx={{ mb: 1 }}>
|
||||||
|
Compare packages
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" aria-live="polite">
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Share + Print */}
|
||||||
|
{(onShare || onPrint) && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
|
||||||
|
{onShare && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
startIcon={<ShareOutlinedIcon />}
|
||||||
|
onClick={onShare}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onPrint && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
startIcon={<PrintOutlinedIcon />}
|
||||||
|
onClick={onPrint}
|
||||||
|
>
|
||||||
|
Print
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Desktop: ComparisonTable */}
|
||||||
|
{!isMobile && (
|
||||||
|
<ComparisonTable packages={allPackages} onArrange={onArrange} onRemove={onRemove} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mobile: Tab rail + card view */}
|
||||||
|
{isMobile && allPackages.length > 0 && (
|
||||||
|
<>
|
||||||
|
{/* Tab rail — mini cards showing provider + package name */}
|
||||||
|
<Box
|
||||||
|
role="tablist"
|
||||||
|
id={tablistId}
|
||||||
|
aria-label="Packages to compare"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1.5,
|
||||||
|
overflowX: 'auto',
|
||||||
|
pb: 1,
|
||||||
|
mb: 2.5,
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
'&::-webkit-scrollbar': { display: 'none' },
|
||||||
|
WebkitOverflowScrolling: 'touch',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{allPackages.map((pkg, idx) => {
|
||||||
|
const isActive = idx === activeTabIdx;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={pkg.id}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={isActive}
|
||||||
|
aria-controls={`comparison-tabpanel-${idx}`}
|
||||||
|
id={`comparison-tab-${idx}`}
|
||||||
|
variant="outlined"
|
||||||
|
selected={isActive}
|
||||||
|
padding="none"
|
||||||
|
onClick={() => setActiveTabIdx(idx)}
|
||||||
|
interactive
|
||||||
|
sx={{
|
||||||
|
flexShrink: 0,
|
||||||
|
minWidth: 150,
|
||||||
|
maxWidth: 200,
|
||||||
|
cursor: 'pointer',
|
||||||
|
...(pkg.isRecommended &&
|
||||||
|
!isActive && {
|
||||||
|
borderColor: 'var(--fa-color-brand-500)',
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ px: 2, py: 1.5 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
mb: 0.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pkg.isRecommended && (
|
||||||
|
<StarRoundedIcon
|
||||||
|
aria-label="Recommended"
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: 'var(--fa-color-brand-600)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="labelSm"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pkg.provider.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{
|
||||||
|
display: 'block',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pkg.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{activePackage && (
|
||||||
|
<Box
|
||||||
|
role="tabpanel"
|
||||||
|
id={`comparison-tabpanel-${activeTabIdx}`}
|
||||||
|
aria-labelledby={`comparison-tab-${activeTabIdx}`}
|
||||||
|
>
|
||||||
|
<MobilePackageCard pkg={activePackage} onArrange={onArrange} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</WizardLayout>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ComparisonPageV1.displayName = 'ComparisonPageV1';
|
||||||
|
export default ComparisonPageV1;
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export { ComparisonPage, default } from './ComparisonPage';
|
export { ComparisonPage, default } from './ComparisonPage';
|
||||||
export type { ComparisonPageProps } from './ComparisonPage';
|
export type { ComparisonPageProps } from './ComparisonPage';
|
||||||
|
export { ComparisonPageV1 } from './ComparisonPageV1';
|
||||||
|
export type { ComparisonPageV1Props } from './ComparisonPageV1';
|
||||||
|
|||||||
Reference in New Issue
Block a user