PackageDetail: add inCart state for the Compare button
When a package is already in the comparison basket, the Compare button swaps to a "In comparison" selected-state: soft brand-50 fill + brand-300 border + brand-700 text + leading check icon. Technically disabled (aria-disabled + no onClick) but sx-overrides the default greyed Mui-disabled look so it reads as "selected/added," not "unavailable." Pattern: e-commerce "Added to cart" state. Removal happens via the floating CompareBar (already owns basket mutation), not this button — keeps the responsibility split clean. API: - PackageDetail: new `inCart?: boolean` prop. - PackagesStep: forwarded as `isSelectedPackageInCart?: boolean`. - Demo route (Packages.tsx): computes `basket.has(key)` for the current selection and passes it through. Storybook: new PackageDetail story `InCart` alongside the existing `Default` and `CompareLoading` states. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -159,6 +159,21 @@ export const CompareLoading: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** "In comparison" selected state — package is already in the basket.
|
||||||
|
* The Compare button is swapped for a soft-brand-tinted inert pill
|
||||||
|
* with a check icon; removal happens via the CompareBar, not here. */
|
||||||
|
export const InCart: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'Traditional Family Cremation Service',
|
||||||
|
price: 6966,
|
||||||
|
sections: [{ heading: 'Essentials', items: essentials.slice(0, 4) }],
|
||||||
|
total: 6966,
|
||||||
|
onArrange: () => alert('Make Arrangement'),
|
||||||
|
onCompare: () => {},
|
||||||
|
inCart: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// --- Without Extras ----------------------------------------------------------
|
// --- Without Extras ----------------------------------------------------------
|
||||||
|
|
||||||
/** Simpler package with essentials and optionals only — no extras */
|
/** Simpler package with essentials and optionals only — no extras */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
|
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
import { Typography } from '../../atoms/Typography';
|
import { Typography } from '../../atoms/Typography';
|
||||||
import { Button } from '../../atoms/Button';
|
import { Button } from '../../atoms/Button';
|
||||||
@@ -53,6 +54,11 @@ export interface PackageDetailProps {
|
|||||||
arrangeDisabled?: boolean;
|
arrangeDisabled?: boolean;
|
||||||
/** Whether the compare button is in loading state */
|
/** Whether the compare button is in loading state */
|
||||||
compareLoading?: boolean;
|
compareLoading?: boolean;
|
||||||
|
/** Whether this package is already in the comparison basket. When true,
|
||||||
|
* the Compare button swaps to an "In comparison" selected-state (soft
|
||||||
|
* brand tint + check icon) and is inert — removal happens via the
|
||||||
|
* CompareBar, not this button. */
|
||||||
|
inCart?: boolean;
|
||||||
/** Custom label for the arrange CTA button (default: "Make Arrangement") */
|
/** Custom label for the arrange CTA button (default: "Make Arrangement") */
|
||||||
arrangeLabel?: string;
|
arrangeLabel?: string;
|
||||||
/** Disclaimer shown below the price (e.g. for unverified/estimated pricing) */
|
/** Disclaimer shown below the price (e.g. for unverified/estimated pricing) */
|
||||||
@@ -124,6 +130,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
|||||||
terms,
|
terms,
|
||||||
onArrange,
|
onArrange,
|
||||||
onCompare,
|
onCompare,
|
||||||
|
inCart = false,
|
||||||
arrangeDisabled = false,
|
arrangeDisabled = false,
|
||||||
compareLoading = false,
|
compareLoading = false,
|
||||||
arrangeLabel = 'Make Arrangement',
|
arrangeLabel = 'Make Arrangement',
|
||||||
@@ -212,18 +219,42 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
|||||||
>
|
>
|
||||||
{arrangeLabel}
|
{arrangeLabel}
|
||||||
</Button>
|
</Button>
|
||||||
{onCompare && (
|
{onCompare &&
|
||||||
<Button
|
(inCart ? (
|
||||||
variant="soft"
|
// Selected-state: soft brand tint + check icon. Inert
|
||||||
color="secondary"
|
// (disabled + aria-disabled) — removal happens via the
|
||||||
size="large"
|
// CompareBar, not this button. sx override keeps the
|
||||||
loading={compareLoading}
|
// brand tint instead of the default greyed-disabled look.
|
||||||
onClick={onCompare}
|
<Button
|
||||||
sx={{ flexShrink: 0 }}
|
variant="outlined"
|
||||||
>
|
color="secondary"
|
||||||
Compare
|
size="large"
|
||||||
</Button>
|
disabled
|
||||||
)}
|
startIcon={<CheckRoundedIcon />}
|
||||||
|
aria-label="Already in comparison"
|
||||||
|
sx={{
|
||||||
|
flexShrink: 0,
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
bgcolor: 'var(--fa-color-brand-50)',
|
||||||
|
borderColor: 'var(--fa-color-brand-300)',
|
||||||
|
color: 'var(--fa-color-text-brand)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
In comparison
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
loading={compareLoading}
|
||||||
|
onClick={onCompare}
|
||||||
|
sx={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
Compare
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export interface PackagesStepProps {
|
|||||||
onArrange: () => void;
|
onArrange: () => void;
|
||||||
/** Callback when the "Compare" button on the PackageDetail panel is clicked */
|
/** Callback when the "Compare" button on the PackageDetail panel is clicked */
|
||||||
onCompare?: () => void;
|
onCompare?: () => void;
|
||||||
|
/** Whether the currently-selected package is already in the comparison
|
||||||
|
* basket. When true, PackageDetail swaps its Compare button into the
|
||||||
|
* "In comparison" selected-state (inert; removal via CompareBar). */
|
||||||
|
isSelectedPackageInCart?: boolean;
|
||||||
/** Callback when a nearby-verified package card is clicked (route change to that provider) */
|
/** Callback when a nearby-verified package card is clicked (route change to that provider) */
|
||||||
onNearbyPackageClick?: (id: string) => void;
|
onNearbyPackageClick?: (id: string) => void;
|
||||||
/**
|
/**
|
||||||
@@ -192,6 +196,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
|||||||
onSelectPackage,
|
onSelectPackage,
|
||||||
onArrange,
|
onArrange,
|
||||||
onCompare,
|
onCompare,
|
||||||
|
isSelectedPackageInCart = false,
|
||||||
onNearbyPackageClick,
|
onNearbyPackageClick,
|
||||||
onSeeAllPackages,
|
onSeeAllPackages,
|
||||||
onProviderClick,
|
onProviderClick,
|
||||||
@@ -275,6 +280,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
|||||||
terms={selectedPackage.terms}
|
terms={selectedPackage.terms}
|
||||||
onArrange={onArrange}
|
onArrange={onArrange}
|
||||||
onCompare={onCompare}
|
onCompare={onCompare}
|
||||||
|
inCart={isSelectedPackageInCart}
|
||||||
arrangeDisabled={loading}
|
arrangeDisabled={loading}
|
||||||
arrangeLabel={copy.arrangeLabel}
|
arrangeLabel={copy.arrangeLabel}
|
||||||
priceDisclaimer={copy.priceDisclaimer}
|
priceDisclaimer={copy.priceDisclaimer}
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ export function PackagesRoute() {
|
|||||||
|
|
||||||
// Compare CTA on the PackageDetail panel just adds the selection to the
|
// Compare CTA on the PackageDetail panel just adds the selection to the
|
||||||
// basket. The floating CompareBar (mounted in App.tsx) handles navigation
|
// basket. The floating CompareBar (mounted in App.tsx) handles navigation
|
||||||
// once the user has 2+ packages selected.
|
// and removal once the user has 2+ packages selected.
|
||||||
const handleCompare = () => {
|
const handleCompare = () => {
|
||||||
if (selectedId) basket.add(makeBasketKey(provider.id, selectedId));
|
if (selectedId) basket.add(makeBasketKey(provider.id, selectedId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When the selected package is already in the basket, PackageDetail swaps
|
||||||
|
// the Compare button into its "In comparison" selected state.
|
||||||
|
const isSelectedInCart = selectedId ? basket.has(makeBasketKey(provider.id, selectedId)) : false;
|
||||||
|
|
||||||
// Tier-3 / tier-2 providers show "nearby verified" cards instead of
|
// Tier-3 / tier-2 providers show "nearby verified" cards instead of
|
||||||
// "more from this provider".
|
// "more from this provider".
|
||||||
const secondaryList =
|
const secondaryList =
|
||||||
@@ -51,6 +55,7 @@ export function PackagesRoute() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onCompare={handleCompare}
|
onCompare={handleCompare}
|
||||||
|
isSelectedPackageInCart={isSelectedInCart}
|
||||||
onNearbyPackageClick={(key) => {
|
onNearbyPackageClick={(key) => {
|
||||||
const [otherProviderId] = key.split(':');
|
const [otherProviderId] = key.split(':');
|
||||||
if (otherProviderId) navigate(`/providers/${otherProviderId}/packages`);
|
if (otherProviderId) navigate(`/providers/${otherProviderId}/packages`);
|
||||||
|
|||||||
Reference in New Issue
Block a user