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 ----------------------------------------------------------
|
||||
|
||||
/** Simpler package with essentials and optionals only — no extras */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
@@ -53,6 +54,11 @@ export interface PackageDetailProps {
|
||||
arrangeDisabled?: boolean;
|
||||
/** Whether the compare button is in loading state */
|
||||
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") */
|
||||
arrangeLabel?: string;
|
||||
/** Disclaimer shown below the price (e.g. for unverified/estimated pricing) */
|
||||
@@ -124,6 +130,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
terms,
|
||||
onArrange,
|
||||
onCompare,
|
||||
inCart = false,
|
||||
arrangeDisabled = false,
|
||||
compareLoading = false,
|
||||
arrangeLabel = 'Make Arrangement',
|
||||
@@ -212,7 +219,31 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
>
|
||||
{arrangeLabel}
|
||||
</Button>
|
||||
{onCompare && (
|
||||
{onCompare &&
|
||||
(inCart ? (
|
||||
// Selected-state: soft brand tint + check icon. Inert
|
||||
// (disabled + aria-disabled) — removal happens via the
|
||||
// CompareBar, not this button. sx override keeps the
|
||||
// brand tint instead of the default greyed-disabled look.
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
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"
|
||||
@@ -223,7 +254,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
>
|
||||
Compare
|
||||
</Button>
|
||||
)}
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -94,6 +94,10 @@ export interface PackagesStepProps {
|
||||
onArrange: () => void;
|
||||
/** Callback when the "Compare" button on the PackageDetail panel is clicked */
|
||||
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) */
|
||||
onNearbyPackageClick?: (id: string) => void;
|
||||
/**
|
||||
@@ -192,6 +196,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
||||
onSelectPackage,
|
||||
onArrange,
|
||||
onCompare,
|
||||
isSelectedPackageInCart = false,
|
||||
onNearbyPackageClick,
|
||||
onSeeAllPackages,
|
||||
onProviderClick,
|
||||
@@ -275,6 +280,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
||||
terms={selectedPackage.terms}
|
||||
onArrange={onArrange}
|
||||
onCompare={onCompare}
|
||||
inCart={isSelectedPackageInCart}
|
||||
arrangeDisabled={loading}
|
||||
arrangeLabel={copy.arrangeLabel}
|
||||
priceDisclaimer={copy.priceDisclaimer}
|
||||
|
||||
@@ -23,11 +23,15 @@ export function PackagesRoute() {
|
||||
|
||||
// Compare CTA on the PackageDetail panel just adds the selection to the
|
||||
// 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 = () => {
|
||||
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
|
||||
// "more from this provider".
|
||||
const secondaryList =
|
||||
@@ -51,6 +55,7 @@ export function PackagesRoute() {
|
||||
)
|
||||
}
|
||||
onCompare={handleCompare}
|
||||
isSelectedPackageInCart={isSelectedInCart}
|
||||
onNearbyPackageClick={(key) => {
|
||||
const [otherProviderId] = key.split(':');
|
||||
if (otherProviderId) navigate(`/providers/${otherProviderId}/packages`);
|
||||
|
||||
Reference in New Issue
Block a user