PackagesStep: surface verified providers via 2-col MiniCard grid
The unverified-tier "similar packages" section previously rendered a list of NearbyPackageCards — one per package. Swap to MiniCard, showing the provider itself: image, verified badge, location, rating, "From $X". 2-col on sm+, 1-col on xs, capped at 4. Heading dropped "nearby" to "Similar packages from verified providers". Data shape renamed NearbyVerifiedPackage → NearbyVerifiedProvider; `verified` is implicit (the section is verified-only by definition). Callback renamed onNearbyPackageClick → onNearbyProviderClick, routing directly on provider id. Demo fixture now derives the list from the main providers fixture (filtered to verified + imageUrl). NearbyPackageCard is now orphaned — kept in place pending registry cleanup in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { PackagesStep } from './PackagesStep';
|
||||
import type { NearbyVerifiedPackage, PackageData, PackagesStepProvider } from './PackagesStep';
|
||||
import type { NearbyVerifiedProvider, PackageData, PackagesStepProvider } from './PackagesStep';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
@@ -211,34 +211,43 @@ const manyOtherPackages: PackageData[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const nearbyVerifiedPackages: NearbyVerifiedPackage[] = [
|
||||
const nearbyVerifiedProviders: NearbyVerifiedProvider[] = [
|
||||
{
|
||||
id: 'rankins-standard',
|
||||
packageName: 'Standard Cremation Package',
|
||||
price: 2450,
|
||||
providerName: 'Rankins Funerals',
|
||||
id: 'rankins',
|
||||
name: 'Rankins Funerals',
|
||||
imageUrl: '/images/placeholder/hparsonsvenue.jpg',
|
||||
location: 'Warrawong, NSW',
|
||||
startingPrice: 2450,
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
},
|
||||
{
|
||||
id: 'easy-essential',
|
||||
packageName: 'Essential Funeral Service',
|
||||
price: 1950,
|
||||
providerName: 'Easy Funerals',
|
||||
location: 'Sydney, NSW',
|
||||
rating: 4.5,
|
||||
id: 'mannings',
|
||||
name: 'Mannings Funerals',
|
||||
imageUrl: '/images/placeholder/hparsonsvenue.jpg',
|
||||
location: 'Bega, NSW',
|
||||
startingPrice: 1950,
|
||||
rating: 4.7,
|
||||
reviewCount: 42,
|
||||
},
|
||||
{
|
||||
id: 'killick-classic',
|
||||
packageName: 'Classic Farewell Package',
|
||||
price: 3100,
|
||||
providerName: 'Killick Family Funerals',
|
||||
location: 'Shellharbour, NSW',
|
||||
id: 'killick',
|
||||
name: 'Killick Family Funerals',
|
||||
imageUrl: '/images/placeholder/hparsonsvenue.jpg',
|
||||
location: 'Kingaroy, QLD',
|
||||
startingPrice: 3100,
|
||||
rating: 4.9,
|
||||
reviewCount: 15,
|
||||
},
|
||||
{
|
||||
id: 'mackay',
|
||||
name: 'Mackay Family Funerals',
|
||||
imageUrl: '/images/placeholder/hparsonsvenue.jpg',
|
||||
location: 'Ourimbah, NSW',
|
||||
startingPrice: 2780,
|
||||
rating: 4.6,
|
||||
reviewCount: 19,
|
||||
},
|
||||
];
|
||||
|
||||
const tier2Packages: PackageData[] = [
|
||||
@@ -362,12 +371,12 @@ export const Tier3: Story = {
|
||||
provider={unverifiedProvider}
|
||||
providerTier="tier3"
|
||||
packages={matchedPackages}
|
||||
secondaryList={{ kind: 'nearby-verified', packages: nearbyVerifiedPackages }}
|
||||
secondaryList={{ kind: 'nearby-verified', providers: nearbyVerifiedProviders }}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Make an enquiry')}
|
||||
onCompare={() => alert('Open compare view')}
|
||||
onNearbyPackageClick={(id) => alert(`Route to nearby package: ${id}`)}
|
||||
onNearbyProviderClick={(id) => alert(`Route to verified provider: ${id}`)}
|
||||
onProviderClick={() => alert('Open provider profile (future)')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
@@ -388,12 +397,12 @@ export const Tier2: Story = {
|
||||
provider={unverifiedProvider}
|
||||
providerTier="tier2"
|
||||
packages={tier2Packages}
|
||||
secondaryList={{ kind: 'nearby-verified', packages: nearbyVerifiedPackages }}
|
||||
secondaryList={{ kind: 'nearby-verified', providers: nearbyVerifiedProviders }}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Make an enquiry')}
|
||||
onCompare={() => alert('Open compare view')}
|
||||
onNearbyPackageClick={(id) => alert(`Route to nearby package: ${id}`)}
|
||||
onNearbyProviderClick={(id) => alert(`Route to verified provider: ${id}`)}
|
||||
onProviderClick={() => alert('Open provider profile (future)')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { WizardLayout } from '../../templates/WizardLayout';
|
||||
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
|
||||
import { ServiceOption } from '../../molecules/ServiceOption';
|
||||
import { NearbyPackageCard } from '../../molecules/NearbyPackageCard';
|
||||
import { MiniCard } from '../../molecules/MiniCard';
|
||||
import { PackageDetail } from '../../organisms/PackageDetail';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
@@ -18,7 +18,7 @@ import type { PackageData, PackagesStepProvider, ProviderTier, SecondaryList } f
|
||||
export type {
|
||||
PackageData,
|
||||
PackagesStepProvider,
|
||||
NearbyVerifiedPackage,
|
||||
NearbyVerifiedProvider,
|
||||
ProviderTier,
|
||||
SecondaryList,
|
||||
} from './types';
|
||||
@@ -75,6 +75,10 @@ const TIER_COPY: Record<ProviderTier, TierCopy> = {
|
||||
// switching to "top N + See all →" behaviour.
|
||||
const SAME_PROVIDER_INLINE_LIMIT = 3;
|
||||
|
||||
// Max number of verified provider MiniCards in the "Similar packages from
|
||||
// verified providers" grid on unverified pages.
|
||||
const NEARBY_VERIFIED_LIMIT = 4;
|
||||
|
||||
// ─── Props ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface PackagesStepProps {
|
||||
@@ -98,8 +102,8 @@ export interface PackagesStepProps {
|
||||
* 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;
|
||||
/** Callback when a nearby-verified provider card is clicked (route change to that provider's PackagesStep) */
|
||||
onNearbyProviderClick?: (id: string) => void;
|
||||
/**
|
||||
* Callback when "See all N packages from [Provider]" is clicked.
|
||||
* Expected to route to the same PackagesStep with `showAllFromProvider` set.
|
||||
@@ -177,7 +181,8 @@ function GroupHeading({
|
||||
* shows top 3 + "See all N packages from [Provider] →" link that routes
|
||||
* to the same page with `showAllFromProvider`.
|
||||
* - `nearby-verified` (unverified tiers): primary list + "Similar packages
|
||||
* from verified providers nearby" list (NearbyPackageCard).
|
||||
* from verified providers" 2-column MiniCard grid, capped at 4. Every
|
||||
* card is verified by definition.
|
||||
*
|
||||
* When `showAllFromProvider` is true, renders a flat "All packages from
|
||||
* [Provider]" list with no grouping and no secondary list. The caller
|
||||
@@ -197,7 +202,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
||||
onArrange,
|
||||
onCompare,
|
||||
isSelectedPackageInCart = false,
|
||||
onNearbyPackageClick,
|
||||
onNearbyProviderClick,
|
||||
onSeeAllPackages,
|
||||
onProviderClick,
|
||||
onBack,
|
||||
@@ -429,29 +434,37 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
||||
|
||||
{/* ─── Secondary: nearby-verified ─── */}
|
||||
{activeSecondaryList?.kind === 'nearby-verified' &&
|
||||
activeSecondaryList.packages.length > 0 && (
|
||||
activeSecondaryList.providers.length > 0 && (
|
||||
<>
|
||||
<Divider sx={{ my: 8 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<VerifiedOutlinedIcon sx={{ fontSize: 16, color: 'primary.main' }} aria-hidden />
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 2 }}>
|
||||
<VerifiedOutlinedIcon
|
||||
sx={{ fontSize: 16, color: 'primary.main', mt: '3px' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
|
||||
Similar packages from verified providers nearby
|
||||
Similar packages from verified providers
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
aria-label="Similar packages from nearby verified providers"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
|
||||
aria-label="Similar packages from verified providers"
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)' },
|
||||
gap: 2,
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{activeSecondaryList.packages.map((pkg) => (
|
||||
<NearbyPackageCard
|
||||
key={pkg.id}
|
||||
packageName={pkg.packageName}
|
||||
price={pkg.price}
|
||||
providerName={pkg.providerName}
|
||||
location={pkg.location}
|
||||
rating={pkg.rating}
|
||||
reviewCount={pkg.reviewCount}
|
||||
onClick={onNearbyPackageClick ? () => onNearbyPackageClick(pkg.id) : undefined}
|
||||
{activeSecondaryList.providers.slice(0, NEARBY_VERIFIED_LIMIT).map((p) => (
|
||||
<MiniCard
|
||||
key={p.id}
|
||||
title={p.name}
|
||||
imageUrl={p.imageUrl}
|
||||
verified
|
||||
price={p.startingPrice}
|
||||
location={p.location}
|
||||
rating={p.rating}
|
||||
onClick={onNearbyProviderClick ? () => onNearbyProviderClick(p.id) : undefined}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -55,19 +55,26 @@ export interface PackageData {
|
||||
terms?: string;
|
||||
}
|
||||
|
||||
/** A package offered by a nearby verified provider (promoted on unverified pages). */
|
||||
export interface NearbyVerifiedPackage {
|
||||
/** Unique ID */
|
||||
/**
|
||||
* A verified provider surfaced on an unverified provider's PackagesStep.
|
||||
*
|
||||
* By definition every entry in this list is verified — the section is a
|
||||
* curated "here are the real partners near you" promotion — so there is no
|
||||
* `verified` flag on the data shape. Components that render this list pass
|
||||
* a hard-coded `verified={true}` to their card.
|
||||
*/
|
||||
export interface NearbyVerifiedProvider {
|
||||
/** Provider ID — routes to `/providers/:id/packages` */
|
||||
id: string;
|
||||
/** Package name */
|
||||
packageName: string;
|
||||
/** Package price in dollars */
|
||||
price: number;
|
||||
/** Provider name */
|
||||
providerName: string;
|
||||
/** Provider location */
|
||||
name: string;
|
||||
/** Hero image URL (verified providers always have one) */
|
||||
imageUrl: string;
|
||||
/** Location (suburb, state) */
|
||||
location: string;
|
||||
/** Provider rating */
|
||||
/** Starting price — formatted as "From $X" on the card */
|
||||
startingPrice: number;
|
||||
/** Average rating */
|
||||
rating?: number;
|
||||
/** Number of reviews */
|
||||
reviewCount?: number;
|
||||
@@ -82,9 +89,10 @@ export interface NearbyVerifiedPackage {
|
||||
* Rendered as a ServiceOption list. If more than 3, the list shows the
|
||||
* first 3 + a "See all N packages from [Provider]" link that navigates
|
||||
* to the same PackagesStep with preference filters off.
|
||||
* - `nearby-verified`: Similar packages from nearby verified providers,
|
||||
* promoted on unverified-tier pages. Rendered as NearbyPackageCard list.
|
||||
* Clicking a card is a route change to that provider's PackagesStep.
|
||||
* - `nearby-verified`: Verified providers promoted on unverified-tier pages
|
||||
* under the heading "Similar packages from verified providers". Rendered
|
||||
* as a 2-col MiniCard grid capped at 4. Clicking a card routes to that
|
||||
* provider's PackagesStep.
|
||||
*/
|
||||
export type SecondaryList =
|
||||
| {
|
||||
@@ -93,5 +101,5 @@ export type SecondaryList =
|
||||
}
|
||||
| {
|
||||
kind: 'nearby-verified';
|
||||
packages: NearbyVerifiedPackage[];
|
||||
providers: NearbyVerifiedProvider[];
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { providersById, toPackagesStepProvider } from '../../../shared/fixtures/
|
||||
import {
|
||||
packagesByProvider,
|
||||
makeBasketKey,
|
||||
nearbyVerifiedSamples,
|
||||
nearbyVerifiedProviders,
|
||||
} from '../../../shared/fixtures/packages';
|
||||
import { useComparisonBasket } from '../../../shared/state/useComparisonBasket';
|
||||
import { demoNav } from '../DemoNav';
|
||||
@@ -34,19 +34,28 @@ export function PackagesRoute() {
|
||||
// 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".
|
||||
// Tier-3 / tier-2 providers show verified-provider MiniCards instead of
|
||||
// "more from this provider". Exclude the current provider from the
|
||||
// "similar" list in case we ever add a verified id that collides.
|
||||
const secondaryList =
|
||||
provider.tier === 'verified'
|
||||
? { kind: 'same-provider-more' as const, packages: bundle.other }
|
||||
: { kind: 'nearby-verified' as const, packages: nearbyVerifiedSamples };
|
||||
: {
|
||||
kind: 'nearby-verified' as const,
|
||||
providers: nearbyVerifiedProviders.filter((p) => p.id !== provider.id),
|
||||
};
|
||||
|
||||
const secondaryHasItems =
|
||||
secondaryList.kind === 'same-provider-more'
|
||||
? secondaryList.packages.length > 0
|
||||
: secondaryList.providers.length > 0;
|
||||
|
||||
return (
|
||||
<PackagesStep
|
||||
provider={toPackagesStepProvider(provider)}
|
||||
providerTier={provider.tier}
|
||||
packages={bundle.matching}
|
||||
secondaryList={secondaryList.packages.length > 0 ? secondaryList : undefined}
|
||||
secondaryList={secondaryHasItems ? secondaryList : undefined}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() =>
|
||||
@@ -58,10 +67,7 @@ export function PackagesRoute() {
|
||||
}
|
||||
onCompare={handleCompare}
|
||||
isSelectedPackageInCart={isSelectedInCart}
|
||||
onNearbyPackageClick={(key) => {
|
||||
const [otherProviderId] = key.split(':');
|
||||
if (otherProviderId) navigate(`/providers/${otherProviderId}/packages`);
|
||||
}}
|
||||
onNearbyProviderClick={(id) => navigate(`/providers/${id}/packages`)}
|
||||
onProviderClick={() => alert('Provider profile — not built in this demo slice.')}
|
||||
onBack={() => navigate('/')}
|
||||
navigation={demoNav}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PackageData, NearbyVerifiedPackage } from '../../../components/pages/PackagesStep';
|
||||
import type { PackageData, NearbyVerifiedProvider } from '../../../components/pages/PackagesStep';
|
||||
import type { PackageSection, PackageLineItem } from '../../../components/organisms/PackageDetail';
|
||||
import type {
|
||||
ComparisonPackage,
|
||||
ComparisonSection,
|
||||
} from '../../../components/organisms/ComparisonTable';
|
||||
import { providersById } from './providers';
|
||||
import { providers, providersById } from './providers';
|
||||
|
||||
/**
|
||||
* Packages live keyed by providerId. Each provider has TWO PackageData lists
|
||||
@@ -876,33 +876,22 @@ export function resolveComparisonPackage(key: BasketKey): ComparisonPackage | nu
|
||||
return bundle.forComparison.find((p) => p.id === parsed.packageId) ?? null;
|
||||
}
|
||||
|
||||
/** "Nearby verified" cards shown under tier-3 / tier-2 lists. */
|
||||
export const nearbyVerifiedSamples: NearbyVerifiedPackage[] = [
|
||||
{
|
||||
id: 'rankins:standard',
|
||||
packageName: 'Standard Cremation Package',
|
||||
price: rankinsForStep[0].price,
|
||||
providerName: 'Rankins Funeral Services',
|
||||
location: 'Wollongong, NSW',
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
},
|
||||
{
|
||||
id: 'mannings:standard',
|
||||
packageName: 'Standard Cremation Package',
|
||||
price: manningsForStep[0].price,
|
||||
providerName: 'Mannings Funerals',
|
||||
location: 'Bega, NSW',
|
||||
rating: 4.7,
|
||||
reviewCount: 31,
|
||||
},
|
||||
{
|
||||
id: 'killick:classic',
|
||||
packageName: 'Classic Farewell Package',
|
||||
price: killickForStep[0].price,
|
||||
providerName: 'Killick Family Funerals',
|
||||
location: 'Kingaroy, QLD',
|
||||
rating: 4.9,
|
||||
reviewCount: 15,
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Verified providers surfaced under the "Similar packages from verified
|
||||
* providers" grid on unverified tier-2 / tier-3 pages. Derived from the
|
||||
* main `providers` fixture filtered to tier === 'verified', with
|
||||
* `startingPrice` taken from their first matching package. Only providers
|
||||
* that actually have an image in the fixture are eligible (MiniCard
|
||||
* requires `imageUrl`).
|
||||
*/
|
||||
export const nearbyVerifiedProviders: NearbyVerifiedProvider[] = providers
|
||||
.filter((p) => p.tier === 'verified' && p.imageUrl)
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
imageUrl: p.imageUrl!,
|
||||
location: p.location,
|
||||
startingPrice: packagesByProvider[p.id]?.matching[0]?.price ?? p.startingPrice ?? 0,
|
||||
rating: p.rating,
|
||||
reviewCount: p.reviewCount,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user