Unify PackagesStep across tiers + polish pass

Consolidate the three tier pages (PackagesStep, UnverifiedPackageT2,
UnverifiedPackageT3) into a single tier-aware PackagesStep with
providerTier: 'verified' | 'tier3' | 'tier2'. Copy, CTA label, price
disclaimer, and itemised-unavailable state all derive from tier via
an internal TIER_COPY map.

Extract NearbyPackageCard as a molecule (was duplicated inline in T2
and T3). Inherits Card atom's default elevated variant so shadow
matches the primary ServiceOption cards in the same column.

Add showAllFromProvider variant for the "See N more packages from
this provider" flow — flat list, no grouping, no secondary list,
preference filter dropped.

Polish pass on PackagesStep + PackageDetail:
- PackageDetail header band warm → white; added card drop-shadow.
- onCompare prop wire-through (button was built in but never exposed).
- Price disclaimer info-box: padding/gap/line-height tuned, icon
  alignment fixed (mt: '3px' matches codebase convention for 16px
  icons paired with body2 text).
- Left-column vertical rhythm: 48px gaps between provider card /
  subheading / list; 128px gap (Divider my: 8) between primary and
  secondary sections to separate groupings.
- Mobile drill-in navigation via useMediaQuery + display toggles.
  onSelectPackage widened to accept string | null; Back button
  swaps to "Back to packages" when a package is selected on mobile.
  Scrolls to top on drill-in.
- "See all" link copy: "See N more packages from this provider →"
  (overflow count, no provider name — sidesteps long-name wrapping).
- Verified provider image: placeholder URL → real local asset
  (hparsonsvenue.jpg, resized 2048×1366/591KB → 640×427/52KB).

Delete legacy PackageSelectPage story in PackageDetail.stories.tsx
(predated the real page components).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 12:45:57 +10:00
parent 312a77aeb9
commit e67872cb6a
17 changed files with 1048 additions and 1553 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -57,6 +57,7 @@ duplicates) and MUST update it after completing one.
| ComparisonPackageCard | done | Card + Button + Divider + Typography + Tooltip + LocationOnOutlinedIcon + VerifiedOutlinedIcon + StarRoundedIcon + CheckCircleOutlineIcon + InfoOutlinedIcon | Mobile full-width package card for ComparisonPage tabpanels. Provider header (inline VerifiedOutlinedIcon left of name when verified, name, location + rating, divider, package name, price block, full-width CTA) + itemised sections with left-accent headings. **Warm tint confined to header only** (not Card body) — Card is white (`background.paper`), header has `surface-warm` (recommended) or `surface-subtle` (verified) bg. **2px brand-600 border** when recommended (matches desktop ComparisonColumnCard). Header `px: 3, pt: 3, pb: 4`. Package-info subgroup (name/label/price) in tight nested flex columns. Generous section spacing (`mb: 5` between sections, `py: 2` per item). Recommended banner at top. Shadow (shadow-sm). Medium full-width button. Reuses `ComparisonPackage` type from ComparisonTable. Shared by ComparisonPage V2 and V1 (extracted 2026-04-09). |
| ComparisonColumnCard | done | Card + Badge + Button + Divider + Typography + Tooltip + Link + StarRoundedIcon + VerifiedOutlinedIcon | Desktop column header card for ComparisonTable. Floating badge: **medium** (26px) filled brand + StarRoundedIcon for recommended; soft brand + VerifiedOutlinedIcon for verified. Provider name **wraps to 2 lines** (`WebkitLineClamp: 2`) in a reserved 36px minHeight slot bottom-aligned so 1-line names anchor with location/rating/price at a consistent baseline. Recommended card: 2px brand-600 border + warm `selected` Card state + inline VerifiedOutlinedIcon left of name. `pt: 5` (40px breathing above name), uniform regardless of verified/recommended. Remove link always renders as the same Link element (visibility-hidden when not applicable) so CTA+footer align across all cards. Per-column wrapper in ComparisonTable is `display: flex` with `flex: 1` passed to the card root so all cards stretch to row height. Extracted from ComparisonTable (2026-04-12). |
| ComparisonTabCard | done | Card + Badge + Typography + StarRoundedIcon | Mobile tab rail card for ComparisonPage. Provider name + package name + price. Recommended badge in normal flow with negative margin overlap — **filled brand + StarRoundedIcon** (matches desktop ComparisonColumnCard treatment, size="small" at 14px icon). **Fixed 235px width** (was 210). Border `brand-600` when recommended (consistent with primary). No glow — uses standard `shadow-sm` like other cards. `pt: 3.5` inside card. Shared by V1 and V2 (extracted 2026-04-12). |
| NearbyPackageCard | done | Card (outlined, interactive) + Typography + StarRoundedIcon + LocationOnOutlinedIcon | Compact card representing a package offered by a nearby verified provider — package name + price + provider + rating + location. Used in the "Similar packages from verified providers nearby" section of PackagesStep for unverified tiers. Click is a route change to that verified provider's PackagesStep with this package loaded. Extracted from UnverifiedPackageT2/T3 during 2026-04-17 consolidation. |
## Organisms
@@ -86,7 +87,7 @@ duplicates) and MUST update it after completing one.
|-----------|--------|-------------|-------|
| IntroStep | done | WizardLayout (centered-form) + ToggleButtonGroup × 2 + Collapse + Typography + Button + Divider | Wizard step 1 — entry point. forWhom (Myself/Someone else) + hasPassedAway (Yes/No) with progressive disclosure. Auto-sets hasPassedAway="no" for "Myself". `<form>` wrapper, aria-live subheading, grief-sensitive copy. Pure presentation. Audit: 18/20 → 20/20 after fixes. |
| ProvidersStep | done | WizardLayout (list-map) + ProviderCard + SearchBar + Chip + Typography + Button | Wizard step 2 — provider selection. List-map split: provider cards w/ radiogroup + search + filter chips (left), map slot (right). aria-live results count, back link. ProviderCard extended with HTML/ARIA passthrough. Audit: 18/20. |
| PackagesStep | done | WizardLayout (list-detail) + ProviderCardCompact + ServiceOption + PackageDetail + Badge + TextField + Typography + Button | Wizard step 3 — package selection. List-detail split: compact provider + budget filter + package list w/ radiogroup (left), PackageDetail breakdown (right). "Most Popular" badge. Mobile Continue button. |
| PackagesStep | done | WizardLayout (list-detail) + ProviderCardCompact + ServiceOption + NearbyPackageCard + PackageDetail + Divider + Link + Typography | Wizard step 3 — package selection. **Tier-aware unified page** (replaces the old PackagesStep + UnverifiedPackageT2 + UnverifiedPackageT3 trio, 2026-04-17). `providerTier: 'verified' \| 'tier3' \| 'tier2'` drives heading, subhead, `arrangeLabel`, `priceDisclaimer`, and `itemizedUnavailable` via a `TIER_COPY` map. Discriminated `secondaryList`: `same-provider-more` (ServiceOption list, verified) or `nearby-verified` (NearbyPackageCard list, unverified). Same-provider-more **shows top 3 inline**; at >3 shows 3 + `See all N packages from [Provider] →` Link that fires `onSeeAllPackages`. `showAllFromProvider` prop renders a flat "All packages from [Provider]" variant (no grouping, no secondary list, preserves `selectedPackageId`). Primary list suppresses the "Matching your preferences" accent-bar heading when no secondary list is present (so the label only appears when there's something to contrast against). Desktop polished; mobile polish pending. |
| ~~PreviewStep~~ | removed | — | Replaced by ArrangementDialog organism (D-E). Package preview + "what's next" checklist now in the dialog's preview step. |
| ~~AuthGateStep~~ | removed | — | Replaced by ArrangementDialog organism (D-E). SSO/email auth flow now in the dialog's auth step. |
| DateTimeStep | done | WizardLayout (centered-form) + Input + TextField (date) + RadioGroup + Collapse + Divider + Button + Link | Wizard step 6 — details & scheduling. Deceased name (Input atom, external label) + preferred dates (up to 3, progressive disclosure) + time-of-day radios. Service tradition removed (flows from provider/package). Dividers between sections. Grief-sensitive labels. Save-and-exit CTA. |

View File

@@ -26,6 +26,58 @@ Each entry follows this structure:
## Sessions
### Session 2026-04-20 — PackageDetail polish + PackagesStep spacing/drill-in + NearbyPackageCard elevation
**Agent(s):** Claude Opus 4.7 (1M context)
**Context:** Continuation of 2026-04-17b (tier consolidation, still uncommitted). This session focused on finishing the PackagesStep page: PackageDetail header/shadow, price disclaimer visual fix, vertical rhythm in the left column, mobile drill-in navigation, and several copy/content refinements. Preflight clean at end of session.
**Work completed:**
**PackageDetail organism:**
- Header band background: `surface-warm``background.paper` (white). Warm tint was competing with the page header and CTA.
- Added `boxShadow: var(--fa-card-shadow-default)` to root for card elevation.
- `Compare` button wire-through: added `onCompare` prop to `PackagesStep`, passed through to `PackageDetail` for all three tiers (button only renders when callback provided — was already built into PackageDetail but never exposed).
- Price-disclaimer info-box refined: padding 12/8 → 16/12, gap 8 → 10, line-height 1.4 → 1.5, icon alignment fix. Initial attempt with a wrapper `Box` centering the icon on full `1.5em` line-box pushed the icon above optical cap-centre (~2px high). Reverted to the codebase convention `mt: '3px'` directly on the icon (matches `PaymentStep` / `CrematoriumStep`).
**PackagesStep page:**
- Vertical-rhythm pass on the left column. Three user-flagged gaps tuned (multiple iterations — 3 → 4 → 6 → 8 on divider, 3 → 4 → 6 on container mb):
- Provider card → h1: `mb: 3``mb: 6` (24 → 48px).
- Subheading → "Matching your preferences" heading: `mb: 3``mb: 6` (24 → 48px).
- Primary list → Divider → secondary section: primary list `mb: 3``mb: 4`, Divider `mb: 2.5``my: 8` (both same-provider-more and nearby-verified dividers). Total gap ≈ 128px + divider line — intentionally larger than 1 & 2 because it separates two distinct groups.
- **Mobile drill-in navigation** (< md breakpoint): added `useMediaQuery` + `mobileShowDetail` derived state. On mobile, list and detail render mutually exclusively via `display: { xs: ..., md: 'block' }` toggles. `onSelectPackage` signature widened to `(id: string | null) => void` so the mobile back button can clear selection. When a package is selected on mobile, the WizardLayout's Back button label/action swaps: `"Back" → onBack` becomes `"Back to packages" → onSelectPackage(null)`. `useEffect` scrolls window to top on drill-in so the detail isn't stranded mid-page. Desktop unchanged (both panels always visible).
- "See all" link copy changed from `See all {total} packages from {provider.name}` to `See {overflow} more packages from this provider` (overflow = `sameProviderPackages.length - SAME_PROVIDER_INLINE_LIMIT`). Rationale: (a) smaller, action-oriented number ("more" implies gain, not total); (b) "this provider" sidesteps the wrap risk on long provider names; (c) the section heading above ("Other packages from [Provider]") already resolves the ambiguous "this provider" reference. Minor imprecision: clicking lands on "All packages from [Provider]" which shows all packages, not just overflow — acceptable ("user asked for more, got the whole picture").
- Verified provider image: placeholder URL → real local asset `/images/placeholder/hparsonsvenue.jpg`. Source was 2048×1366 / 591KB; resized in place to 640×427 / 52KB via ImageMagick (quality 82, metadata stripped). Provider card displays at 120160px wide so 640 is plenty for 2× retina.
**NearbyPackageCard molecule:**
- Dropped `variant="outlined"` override — now inherits `Card` atom's default `elevated` (shadow). Matches the primary `ServiceOption` cards in the same column. Per design discussion: shadow signals "interactive, lifts on engage" for selectable/linked cards; outlined reserved for context/container cards.
**ProviderCardCompact:**
- Kept outlined (discussed, not changed). Different role: page-level context header, not part of the selection group. Shadow across everything in the column would flatten hierarchy.
**Decisions made:**
- **Icon alignment convention reaffirmed**: 16px icons with body2 text use `mt: '3px'` directly on the icon with `alignItems: 'flex-start'` on the parent. Don't wrap the icon in a flex-center box — MUI icons have SVG padding that puts geometric centre above optical cap-centre.
- **Shadow vs outlined**: interactive content cards (ServiceOption, NearbyPackageCard) use shadow; context/container cards (ProviderCardCompact) use outlined. Avoids flattening visual hierarchy.
- **Mobile drill-in over inline-expand or bottom-sheet**: cleanest state model (selection drives view), well-established pattern, no new components.
- **"See X more" over "See all X"**: smaller framing, sidesteps long-name wrap.
**Figma / Make exploration:**
- Captured PackagesStep `Tier2` story to the Parsons Figma file (https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT?node-id=6073-25005) for Gemini 3 polish pass in Figma Make.
- Drafted two Gemini 3 prompts tailored to Figma Make — one for polish (constraints-last, preserves structure), one for mobile exploration (3 directions: drill-in, inline-expand, bottom-sheet). User chose drill-in for implementation.
- Research notes: Gemini 3 weights tail of prompt heavily; prefers TC-EBC framework (Task / Context / Boundaries / Criteria); terse over verbose; declare design-system constraints upfront.
**Preflight:** TS, ESLint, Prettier (auto-fixed 4 files), Storybook build, token sync, exports all PASS. Hardcoded-hex scan: only pre-existing values in other files; none introduced in this session's edits.
**Open questions:**
- None.
**Next steps:**
- Carry-overs from ComparisonPage 2026-04-17 still open: [P1] mobile tab rail arrow-key nav, [P2] dot indicator tap targets (44px min), [P3] desktop empty state, plus the deferred collapsing sticky header.
- MapCard molecule still "planned" in registry — deferred until map integration.
- Provider profile page — `onProviderClick` is wired but destination doesn't exist yet.
---
### Session 2026-04-17 — ComparisonPage restructure (scroll model, sticky-left, tiered hover) + card refinements + mobile polish
**Agent(s):** Claude Opus 4.7 (1M context)
@@ -96,9 +148,59 @@ Each entry follows this structure:
- None blocking.
**Next steps:**
- Commit today's work (2 commits: Phase A+B desktop restructure, then card refinements + mobile polish).
- Optional: `/audit` on refreshed ComparisonPage + ComparisonTable; `/critique` on the mobile and desktop views.
- User flagged next focus areas (not started this session): package select page refinements; map pins / map cards (MapCard molecule is still "planned" in the registry).
- Committed in 2 commits (f146bb0f restructure + 312a77ae mobile polish). Branch is 10 commits ahead of origin/main.
- **User-flagged next focus areas** (the reason this session ended): (1) Package Select page refinements; (2) Map pins (MapPin atom is done, MapPopup molecule is done, **MapCard molecule is still "planned"** in the registry — deferred until map integration).
- **Audit run at end of session — scored 19/20 (Excellent). Carry-overs**:
- **[P1] Mobile tab rail arrow-key navigation** — `role="tablist"` expects Left/Right to move focus between tabs, Home/End to jump first/last. Currently Tab into first tab works, but arrows don't move focus. ~20 lines: keyboard handler on tablist moving `activeTabIdx`.
- **[P2] Dot indicator tap targets ~24px** — below WCAG 2.5.5 (44×44). Acceptable as a supplement (tab rail is canonical nav) but worth bumping padding from `p: 1` to `p: 1.5` or wrapping in a larger hit region with visually 8px dot.
- **[P3] Empty-state on desktop** — if user removes all packages, desktop renders header + divider + empty table zone. Mobile already guards `allPackages.length > 0`; desktop branch should mirror.
- **[P3] Info icon tooltips on mobile** — 14px tap + long-press affordance. User has explicitly accepted as exception; noted for record.
- **Deferred from this session (ready to rebuild, machinery stripped cleanly)**: the collapsing sticky mini-header on desktop ComparisonTable. Design: full ComparisonColumnCard row `position: sticky; top: 0`, sentinel + IntersectionObserver detects when full row scrolls out, collapses to a mini row showing provider + price + CTA. User wants to pick this up later.
---
### Session 2026-04-17b — PackagesStep tier consolidation + NearbyPackageCard + "See all" variant
**Agent(s):** Claude Opus 4.7 (1M context)
**Context:** User asked to continue work on the package select page. Before touching code, scoped out the consolidation: three separate pages (`PackagesStep`, `UnverifiedPackageT3`, `UnverifiedPackageT2`) were structurally identical modulo a handful of tier-driven differences — provider image, heading copy, secondary-list content, and `PackageDetail` flags. Same layout, triplicated. Plus a legacy `PackageSelectPage` story inside `PackageDetail.stories.tsx` predating the real page components.
**Decisions made:**
- **One unified `PackagesStep` with `providerTier: 'verified' | 'tier3' | 'tier2'`** rather than per-tier pages. Reasoning: every polish pass would otherwise be triplicated (the Comparison work last session landed 20+ small touches — that's a real tax); differences are all *data/copy*, not *layout*. `PackageDetail` already validated this pattern.
- **Copy/CTA/disclaimer/itemized-unavailable all derived from tier** via an internal `TIER_COPY` map. Business defaults live in the page, `PackageDetail` stays prop-driven and tier-agnostic.
- **Discriminated `secondaryList` union**: `{ kind: 'same-provider-more', packages }` (verified) vs `{ kind: 'nearby-verified', packages }` (unverified). Rendering branches cleanly from the discriminator; types force callers to pick the right shape.
- **Routing model for "See all" + package clicks**: URL-driven — every (provider, package, preferences) triple has its own route. Clicking a nearby-verified package → route change to that provider's PackagesStep. "See all N packages from [Provider]" → route change to the *same* PackagesStep with `showAllFromProvider=true` (preference-filter dropped). Component doesn't own "show all" state; caller hands in the full list and flips the flag.
- **`showAllFromProvider` variant**: flat list, title becomes "All packages from [Provider]", no grouping, no secondary list, `selectedPackageId` preserved from the origin view. Subhead "Every package [Provider] offers, including those outside your preferences."
- **>3 rule** for same-provider-more: show first 3 + "See all N packages from [Provider] →" Link (MUI Link with `component="button"` + `ArrowForwardIcon`). Below the limit, render all inline with no link. Callback is `onSeeAllPackages`.
- **"Matching your preferences" accent-bar heading suppressed when no secondary list follows** — same rule as before; the label only appears when there's a contrasting group below it. Also suppressed in `showAllFromProvider` mode.
- **`NearbyPackageCard` extracted as a molecule** rather than kept inline. ~50 lines of bespoke JSX was duplicated across T2 and T3 — now a single molecule with its own stories (`Default`, `WithoutRating`, `Static`, `Stacked`).
- **Legacy `PackageSelectPage` story deleted** from `PackageDetail.stories.tsx`. Story was a page-level mock predating the real page components — misleading dead weight. Unused imports/helpers pruned (`useState`, `ServiceOption`, `ProviderCardCompact`, `Chip`, `Typography`, `Button`, `Navigation`, `ArrowBackIcon`, `DEMO_IMAGE`, `packages`, `funeralTypes`, `FALogoNav`).
**Work completed:**
- New molecule: `src/components/molecules/NearbyPackageCard/` (tsx + stories + index).
- New shared types file: `src/components/pages/PackagesStep/types.ts``ProviderTier`, `PackagesStepProvider`, `PackageData`, `NearbyVerifiedPackage`, `SecondaryList` discriminated union. Page re-exports them for callers.
- Rewrote `src/components/pages/PackagesStep/PackagesStep.tsx` as the tier-aware unified component. `TIER_COPY` map, `SAME_PROVIDER_INLINE_LIMIT = 3`, `GroupHeading` local helper (accent bar + label, primary/secondary emphasis).
- Rewrote `PackagesStep.stories.tsx` — 9 stories: `Verified`, `VerifiedWithManyOtherPackages` (exercises >3 rule), `AllFromProvider` (showAllFromProvider variant), `Tier3`, `Tier2`, `NoSelection`, `VerifiedNoSecondary`, `PrePlanning`, `WithError`.
- Deleted `src/components/pages/UnverifiedPackageT2/` and `src/components/pages/UnverifiedPackageT3/` entirely.
- Deleted the legacy `PackageSelectPage` story in `PackageDetail.stories.tsx` and pruned orphaned imports/helpers.
- Registry: added `NearbyPackageCard` row to Molecules; rewrote `PackagesStep` row to describe the tier-aware shape and the `showAllFromProvider` variant.
**Non-blocking TODOs noted:**
- **Provider profile page** (future) — `onProviderClick` is wired up on the provider card but the destination isn't built yet.
- **Mobile polish** — desktop only this pass (by user instruction); mobile layout to be reconsidered after the desktop shape settles. Current mobile state is the `list-detail` WizardLayout's stacked fallback; not visually tuned.
**Preflight:** typecheck clean, lint clean.
**Open questions:**
- None blocking.
**Next steps:**
- Uncommitted. Expect a single commit for the consolidation.
- Mobile pass on the unified PackagesStep next.
- Provider profile page when it comes up.
---

View File

@@ -0,0 +1,93 @@
import type { Meta, StoryObj } from '@storybook/react';
import Box from '@mui/material/Box';
import { NearbyPackageCard } from './NearbyPackageCard';
const meta: Meta<typeof NearbyPackageCard> = {
title: 'Molecules/NearbyPackageCard',
component: NearbyPackageCard,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
decorators: [
(Story) => (
<Box sx={{ maxWidth: 480, width: '100%' }}>
<Story />
</Box>
),
],
};
export default meta;
type Story = StoryObj<typeof NearbyPackageCard>;
/** Default — full metadata including rating and review count */
export const Default: Story = {
args: {
packageName: 'Everyday Cremation',
price: 4200,
providerName: 'H.Parsons Funerals',
location: 'Wentworth',
rating: 4.5,
reviewCount: 32,
onClick: () => alert('Navigate to provider package'),
},
};
/** Without rating — provider has no reviews yet */
export const WithoutRating: Story = {
args: {
packageName: 'Simple Farewell',
price: 3800,
providerName: 'Riverstone Funerals',
location: 'Mildura',
onClick: () => alert('Navigate to provider package'),
},
};
/** Non-interactive — no onClick */
export const Static: Story = {
args: {
packageName: 'Everyday Cremation',
price: 4200,
providerName: 'H.Parsons Funerals',
location: 'Wentworth',
rating: 4.5,
reviewCount: 32,
},
};
/** Stacked — as rendered in the similar-packages list */
export const Stacked: Story = {
render: () => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<NearbyPackageCard
packageName="Everyday Cremation"
price={4200}
providerName="H.Parsons Funerals"
location="Wentworth"
rating={4.5}
reviewCount={32}
onClick={() => {}}
/>
<NearbyPackageCard
packageName="Traditional Farewell"
price={6800}
providerName="Mackay Family Funerals"
location="Parramatta"
rating={4.7}
reviewCount={58}
onClick={() => {}}
/>
<NearbyPackageCard
packageName="Simple Cremation"
price={3500}
providerName="Coastal Funerals"
location="Cronulla"
rating={4.3}
reviewCount={14}
onClick={() => {}}
/>
</Box>
),
};

View File

@@ -0,0 +1,106 @@
import React from 'react';
import Box from '@mui/material/Box';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import type { SxProps, Theme } from '@mui/material/styles';
import { Card } from '../../atoms/Card';
import { Typography } from '../../atoms/Typography';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA NearbyPackageCard molecule */
export interface NearbyPackageCardProps {
/** Package display name */
packageName: string;
/** Package price in dollars */
price: number;
/** Provider display name */
providerName: string;
/** Provider location (suburb, city) */
location: string;
/** Provider rating (e.g. 4.5). Omit to hide. */
rating?: number;
/** Number of reviews */
reviewCount?: number;
/** Click handler — navigates to that provider's PackagesStep with this package loaded */
onClick?: () => void;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Compact card representing a package offered by a nearby verified provider.
*
* Surfaced in the "Similar packages from verified providers nearby" section
* of the unverified-tier PackagesStep pages. Clicking the card is a route
* change to that verified provider's PackagesStep with this package loaded.
*
* Composes Card + Typography.
*/
export const NearbyPackageCard = React.forwardRef<HTMLDivElement, NearbyPackageCardProps>(
({ packageName, price, providerName, location, rating, reviewCount, onClick, sx }, ref) => {
return (
<Card
ref={ref}
interactive={!!onClick}
padding="none"
onClick={onClick}
sx={[{ p: 'var(--fa-card-padding-compact)' }, ...(Array.isArray(sx) ? sx : [sx])]}
>
{/* Package name + price */}
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: 2,
mb: 1,
}}
>
<Typography variant="h6" component="span">
{packageName}
</Typography>
<Typography
variant="labelLg"
component="span"
color="primary"
sx={{ whiteSpace: 'nowrap' }}
>
${price.toLocaleString('en-AU')}
</Typography>
</Box>
{/* Provider info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary">
{providerName}
</Typography>
{rating != null && (
<>
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<StarRoundedIcon sx={{ fontSize: 14, color: 'warning.main' }} aria-hidden />
<Typography variant="caption" color="text.secondary">
{rating}
{reviewCount != null ? ` (${reviewCount})` : ''}
</Typography>
</>
)}
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<LocationOnOutlinedIcon sx={{ fontSize: 14, color: 'text.secondary' }} aria-hidden />
<Typography variant="caption" color="text.secondary">
{location}
</Typography>
</Box>
</Card>
);
},
);
NearbyPackageCard.displayName = 'NearbyPackageCard';
export default NearbyPackageCard;

View File

@@ -0,0 +1 @@
export { NearbyPackageCard, type NearbyPackageCardProps } from './NearbyPackageCard';

View File

@@ -1,17 +1,6 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import Box from '@mui/material/Box';
import { PackageDetail } from './PackageDetail';
import { ServiceOption } from '../../molecules/ServiceOption';
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
import { Chip } from '../../atoms/Chip';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
import { Navigation } from '../Navigation';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
const DEMO_IMAGE =
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
const essentials = [
{
@@ -117,41 +106,6 @@ const extras = {
const termsText =
'* This package includes a funeral service at a chapel or a church with a funeral procession following to the crematorium. It includes many of the most commonly selected funeral options preselected for you. Many people choose this package for the extended funeral rituals — of course, you can tailor the funeral service to meet your needs and budget as you go through the selections.';
const packages = [
{
id: 'everyday',
name: 'Everyday Funeral Package',
price: 900,
description:
'Our most popular package with all essential services included. Suitable for a traditional chapel or church service.',
},
{
id: 'deluxe',
name: 'Deluxe Funeral Package',
price: 1200,
description: 'An enhanced package with premium coffin and additional floral arrangements.',
},
{
id: 'essential',
name: 'Essential Funeral Package',
price: 600,
description: 'A simple, dignified service covering all necessary arrangements.',
},
{
id: 'catholic',
name: 'Catholic Service',
price: 950,
description:
'A service tailored for Catholic traditions including prayers and church ceremony.',
},
];
const funeralTypes = ['All', 'Cremation', 'Burial', 'Memorial', 'Catholic', 'Direct Cremation'];
const FALogoNav = () => (
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 28 }} />
);
const meta: Meta<typeof PackageDetail> = {
title: 'Organisms/PackageDetail',
component: PackageDetail,
@@ -222,132 +176,3 @@ export const WithoutExtras: Story = {
onCompare: () => alert('Compare'),
},
};
// --- Package Select Page Layout ----------------------------------------------
/** Full page layout — left: package list, right: detail panel */
export const PackageSelectPage: Story = {
decorators: [
(Story) => (
<Box sx={{ maxWidth: 'none', width: '100%' }}>
<Story />
</Box>
),
],
render: () => {
const [selectedPkg, setSelectedPkg] = useState('everyday');
const [activeFilter, setActiveFilter] = useState('Cremation');
const [comparing, setComparing] = useState(false);
const handleCompare = () => {
setComparing(true);
setTimeout(() => setComparing(false), 1500);
};
return (
<Box>
<Navigation
logo={<FALogoNav />}
items={[
{ label: 'Provider Portal', href: '/provider-portal' },
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Log in', href: '/login' },
]}
/>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
gap: { xs: 3, md: 4 },
maxWidth: 'lg',
mx: 'auto',
px: { xs: 2, md: 4 },
py: { xs: 2, md: 4 },
alignItems: 'start',
}}
>
{/* Left column */}
<Box>
<Button
variant="text"
color="secondary"
startIcon={<ArrowBackIcon />}
sx={{ mb: 2, ml: -1 }}
>
Back
</Button>
<Typography variant="h2" sx={{ mb: 3 }}>
Select a package
</Typography>
<ProviderCardCompact
name="H.Parsons"
location="Wentworth"
imageUrl={DEMO_IMAGE}
rating={4.5}
reviewCount={11}
sx={{ mb: 3 }}
/>
{/* Funeral type filter */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{funeralTypes.map((type) => (
<Chip
key={type}
label={type}
variant={activeFilter === type ? 'filled' : 'outlined'}
selected={activeFilter === type}
onClick={() => setActiveFilter(type)}
size="small"
/>
))}
</Box>
<Typography variant="h4" sx={{ mb: 2 }}>
Packages
</Typography>
<Box
role="radiogroup"
aria-label="Available packages"
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
>
{packages.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
price={pkg.price}
description={pkg.description}
selected={selectedPkg === pkg.id}
onClick={() => setSelectedPkg(pkg.id)}
maxDescriptionLines={2}
/>
))}
</Box>
</Box>
{/* Right column: package detail */}
<Box sx={{ position: { md: 'sticky' }, top: { md: 96 } }}>
<PackageDetail
name={packages.find((p) => p.id === selectedPkg)?.name ?? ''}
price={packages.find((p) => p.id === selectedPkg)?.price ?? 0}
sections={[
{ heading: 'Essentials', items: essentials },
{ heading: 'Optionals', items: optionals },
]}
total={6966}
extras={extras}
terms={termsText}
onArrange={() => alert(`Making arrangement for: ${selectedPkg}`)}
onCompare={handleCompare}
compareLoading={comparing}
/>
</Box>
</Box>
</Box>
);
},
};

View File

@@ -141,6 +141,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
border: '1px solid',
borderColor: 'divider',
borderRadius: 'var(--fa-card-border-radius-default)',
boxShadow: 'var(--fa-card-shadow-default)',
overflow: 'hidden',
},
...(Array.isArray(sx) ? sx : [sx]),
@@ -149,7 +150,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
{/* Header band — warm bg to separate from content */}
<Box
sx={{
bgcolor: 'var(--fa-color-surface-warm)',
bgcolor: 'background.paper',
px: { xs: 2, sm: 3 },
pt: 3,
pb: 2.5,
@@ -178,10 +179,10 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 1,
gap: 1.25,
mt: 1.5,
px: 1.5,
py: 1,
px: 2,
py: 1.5,
bgcolor: 'var(--fa-color-surface-cool, #F5F7FA)',
borderRadius: 'var(--fa-border-radius-sm, 6px)',
border: '1px solid',
@@ -189,10 +190,10 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
}}
>
<InfoOutlinedIcon
sx={{ fontSize: 16, color: 'text.secondary', mt: '1px', flexShrink: 0 }}
sx={{ fontSize: 16, color: 'text.secondary', mt: '3px', flexShrink: 0 }}
aria-hidden
/>
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.4 }}>
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.5 }}>
{priceDisclaimer}
</Typography>
</Box>

View File

@@ -1,9 +1,9 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { PackagesStep } from './PackagesStep';
import type { PackageData, PackagesStepProvider } from './PackagesStep';
import { Navigation } from '../../organisms/Navigation';
import Box from '@mui/material/Box';
import { PackagesStep } from './PackagesStep';
import type { NearbyVerifiedPackage, PackageData, PackagesStepProvider } from './PackagesStep';
import { Navigation } from '../../organisms/Navigation';
// ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -35,10 +35,19 @@ const nav = (
/>
);
const mockProvider: PackagesStepProvider = {
// ─── Mock data ───────────────────────────────────────────────────────────────
const verifiedProvider: PackagesStepProvider = {
name: 'H.Parsons Funeral Directors',
location: 'Wentworth, NSW',
imageUrl: '/images/placeholder/hparsonsvenue.jpg',
rating: 4.6,
reviewCount: 7,
};
const unverifiedProvider: PackagesStepProvider = {
name: 'H.Parsons Funeral Directors',
location: 'Wentworth, NSW',
imageUrl: 'https://placehold.co/120x80/E8E0D6/8B6F47?text=H.Parsons',
rating: 4.6,
reviewCount: 7,
};
@@ -147,6 +156,110 @@ const otherPackages: PackageData[] = [
},
];
const manyOtherPackages: PackageData[] = [
...otherPackages,
{
id: 'memorial',
name: 'Memorial Service',
price: 2400,
description: 'A celebration-of-life service without burial or cremation on the same day.',
sections: [
{
heading: 'Essentials',
items: [
{ name: 'Professional Service Fee', price: 1200 },
{ name: 'Venue coordination', price: 600 },
{ name: 'Memorial book', price: 100 },
],
},
],
total: 2400,
},
{
id: 'graveside',
name: 'Graveside Service',
price: 2900,
description: 'A simple graveside committal, ideal for smaller family gatherings.',
sections: [
{
heading: 'Essentials',
items: [
{ name: 'Professional Mortuary Care', price: 1000 },
{ name: 'Professional Service Fee', price: 1100 },
{ name: 'Cemetery coordination', price: 400 },
],
},
],
total: 2900,
},
{
id: 'prepaid-basic',
name: 'Prepaid Basic Plan',
price: 3600,
description: 'Lock in todays price for a basic cremation package, paid over 12 months.',
sections: [
{
heading: 'Essentials',
items: [
{ name: 'Locked-in pricing', price: 0, priceLabel: 'Complimentary' },
{ name: 'Professional Service Fee', price: 1200 },
{ name: 'Professional Mortuary Care', price: 1000 },
],
},
],
total: 3600,
},
];
const nearbyVerifiedPackages: NearbyVerifiedPackage[] = [
{
id: 'rankins-standard',
packageName: 'Standard Cremation Package',
price: 2450,
providerName: 'Rankins Funerals',
location: 'Warrawong, NSW',
rating: 4.8,
reviewCount: 23,
},
{
id: 'easy-essential',
packageName: 'Essential Funeral Service',
price: 1950,
providerName: 'Easy Funerals',
location: 'Sydney, NSW',
rating: 4.5,
reviewCount: 42,
},
{
id: 'killick-classic',
packageName: 'Classic Farewell Package',
price: 3100,
providerName: 'Killick Family Funerals',
location: 'Shellharbour, NSW',
rating: 4.9,
reviewCount: 15,
},
];
const tier2Packages: PackageData[] = [
{
id: 't2-standard',
name: 'Standard Funeral Service',
price: 5200,
description:
'A full-service package based on publicly available information. Breakdown not available — make an enquiry to confirm what is included.',
sections: [],
},
{
id: 't2-basic',
name: 'Basic Cremation',
price: 3400,
description:
'An entry-level package based on publicly available information. Pricing is indicative only.',
sections: [],
},
];
// ─── Meta ────────────────────────────────────────────────────────────────────
const meta: Meta<typeof PackagesStep> = {
@@ -161,45 +274,24 @@ const meta: Meta<typeof PackagesStep> = {
export default meta;
type Story = StoryObj<typeof PackagesStep>;
// ─── Interactive (default) ──────────────────────────────────────────────────
// ─── Verified ────────────────────────────────────────────────────────────────
/** Matched + other packages — select a package, see detail, click Make Arrangement */
export const Default: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<PackagesStep
provider={mockProvider}
packages={matchedPackages}
otherPackages={otherPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── With selection ─────────────────────────────────────────────────────────
/** Package already selected — detail panel visible */
export const WithSelection: Story = {
/** Verified provider — matching packages + up to 3 other packages from the same provider */
export const Verified: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>('everyday');
return (
<PackagesStep
provider={mockProvider}
provider={verifiedProvider}
providerTier="verified"
packages={matchedPackages}
otherPackages={otherPackages}
secondaryList={{ kind: 'same-provider-more', packages: otherPackages }}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onCompare={() => alert('Open compare view')}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
/>
@@ -207,21 +299,127 @@ export const WithSelection: Story = {
},
};
// ─── No other packages (all match) ─────────────────────────────────────────
// ─── Verified — with "See all" link ─────────────────────────────────────────
/** All packages match filters — no "Other packages" section */
export const AllMatching: Story = {
/** Verified provider with 5+ other packages — shows first 3 + "See all N packages" link */
export const VerifiedWithManyOtherPackages: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>('everyday');
return (
<PackagesStep
provider={verifiedProvider}
providerTier="verified"
packages={matchedPackages}
secondaryList={{ kind: 'same-provider-more', packages: manyOtherPackages }}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onSeeAllPackages={() => alert('Route to showAllFromProvider variant')}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── "Show all from provider" variant ───────────────────────────────────────
/** Flat "All packages from [Provider]" view — no grouping, selected package preserved */
export const AllFromProvider: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>('everyday');
const allPackages = [...matchedPackages, ...manyOtherPackages];
return (
<PackagesStep
provider={verifiedProvider}
providerTier="verified"
packages={allPackages}
showAllFromProvider
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onCompare={() => alert('Open compare view')}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── Tier 3 (itemised breakdown) ────────────────────────────────────────────
/** Tier 3 unverified — itemised breakdown + "Make an enquiry" + nearby verified alternatives */
export const Tier3: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>('everyday');
return (
<PackagesStep
provider={unverifiedProvider}
providerTier="tier3"
packages={matchedPackages}
secondaryList={{ kind: 'nearby-verified', packages: nearbyVerifiedPackages }}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Make an enquiry')}
onCompare={() => alert('Open compare view')}
onNearbyPackageClick={(id) => alert(`Route to nearby package: ${id}`)}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── Tier 2 (price only, no breakdown) ──────────────────────────────────────
/** Tier 2 unverified — price only, detail panel shows "Itemised Pricing Unavailable" */
export const Tier2: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>('t2-standard');
return (
<PackagesStep
provider={unverifiedProvider}
providerTier="tier2"
packages={tier2Packages}
secondaryList={{ kind: 'nearby-verified', packages: nearbyVerifiedPackages }}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Make an enquiry')}
onCompare={() => alert('Open compare view')}
onNearbyPackageClick={(id) => alert(`Route to nearby package: ${id}`)}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── Edge cases ──────────────────────────────────────────────────────────────
/** No selection yet — empty detail panel */
export const NoSelection: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<PackagesStep
provider={mockProvider}
packages={[...matchedPackages, ...otherPackages]}
provider={verifiedProvider}
providerTier="verified"
packages={matchedPackages}
secondaryList={{ kind: 'same-provider-more', packages: otherPackages }}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onCompare={() => alert('Open compare view')}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
/>
@@ -229,7 +427,27 @@ export const AllMatching: Story = {
},
};
// ─── Pre-planning ───────────────────────────────────────────────────────────
/** Verified provider with no "other packages" — primary list only */
export const VerifiedNoSecondary: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<PackagesStep
provider={verifiedProvider}
providerTier="verified"
packages={matchedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onCompare={() => alert('Open compare view')}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
/** Pre-planning flow — softer copy */
export const PrePlanning: Story = {
@@ -238,13 +456,15 @@ export const PrePlanning: Story = {
return (
<PackagesStep
provider={mockProvider}
provider={verifiedProvider}
providerTier="verified"
packages={matchedPackages}
otherPackages={otherPackages}
secondaryList={{ kind: 'same-provider-more', packages: otherPackages }}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onCompare={() => alert('Open compare view')}
onProviderClick={() => alert('Open provider profile (future)')}
onBack={() => alert('Back')}
navigation={nav}
isPrePlanning
@@ -253,16 +473,15 @@ export const PrePlanning: Story = {
},
};
// ─── Validation error ───────────────────────────────────────────────────────
/** Error shown when no package selected */
/** Validation error */
export const WithError: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<PackagesStep
provider={mockProvider}
provider={verifiedProvider}
providerTier="verified"
packages={matchedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}

View File

@@ -1,68 +1,117 @@
import React from 'react';
import React, { useEffect } from 'react';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
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 { PackageDetail } from '../../organisms/PackageDetail';
import type { PackageSection } from '../../organisms/PackageDetail';
import { Typography } from '../../atoms/Typography';
import { Divider } from '../../atoms/Divider';
import { Link } from '../../atoms/Link';
import type { PackageData, PackagesStepProvider, ProviderTier, SecondaryList } from './types';
// ─── Types ───────────────────────────────────────────────────────────────────
export type {
PackageData,
PackagesStepProvider,
NearbyVerifiedPackage,
ProviderTier,
SecondaryList,
} from './types';
/** Provider summary for the compact card */
export interface PackagesStepProvider {
/** Provider name */
name: string;
/** Location */
location: string;
/** Image URL */
imageUrl?: string;
/** Rating */
rating?: number;
/** Review count */
reviewCount?: number;
// ─── Tier copy map ───────────────────────────────────────────────────────────
interface TierCopy {
heading: string;
subheading: (isPrePlanning: boolean) => string;
arrangeLabel: string;
priceDisclaimer?: string;
itemizedUnavailable: boolean;
emptyDetailMessage: string;
}
/** Package data for the selection list */
export interface PackageData {
/** Unique package ID */
id: string;
/** Package display name */
name: string;
/** Package price in dollars */
price: number;
/** Short description */
description?: string;
/** Line item sections for the detail panel */
sections: PackageSection[];
/** Total price (may differ from base price with extras) */
total?: number;
/** Extra items section (after total) */
extras?: PackageSection;
/** Terms and conditions */
terms?: string;
}
const TIER_COPY: Record<ProviderTier, TierCopy> = {
verified: {
heading: 'Choose a funeral package',
subheading: (isPrePlanning) =>
isPrePlanning
? 'Compare packages to find what suits your wishes. Nothing is committed until you confirm.'
: 'Each package includes a set of services. You can customise your selections in the next steps.',
arrangeLabel: 'Make Arrangement',
itemizedUnavailable: false,
emptyDetailMessage: "Select a package to see what's included.",
},
tier3: {
heading: 'Explore available packages',
subheading: (isPrePlanning) =>
isPrePlanning
? 'Browse estimated packages to get a sense of what this provider offers. Nothing is committed.'
: 'These packages are based on publicly available information. Make an enquiry to confirm details and pricing.',
arrangeLabel: 'Make an enquiry',
priceDisclaimer:
"Prices are estimates based on publicly available information and may not reflect the provider's current pricing.",
itemizedUnavailable: false,
emptyDetailMessage: "Select a package to see what's included.",
},
tier2: {
heading: 'Explore available packages',
subheading: (isPrePlanning) =>
isPrePlanning
? 'Browse estimated packages to get a sense of what this provider offers. Nothing is committed.'
: 'These packages are based on publicly available information. Make an enquiry to confirm details and pricing.',
arrangeLabel: 'Make an enquiry',
priceDisclaimer:
"Prices are estimates based on publicly available information and may not reflect the provider's current pricing.",
itemizedUnavailable: true,
emptyDetailMessage: 'Select a package to see more details.',
},
};
// Show at most this many "other packages from this provider" inline before
// switching to "top N + See all →" behaviour.
const SAME_PROVIDER_INLINE_LIMIT = 3;
// ─── Props ───────────────────────────────────────────────────────────────────
/** Props for the PackagesStep page component */
export interface PackagesStepProps {
/** Provider summary shown at top of the list panel */
/** Provider shown at the top of the list panel */
provider: PackagesStepProvider;
/** Packages matching the user's filters from the previous step */
/** Provider tier — drives copy, CTA label, disclaimer, itemised-unavailable state */
providerTier: ProviderTier;
/** Packages in the primary list (filtered by user preferences, or all when `showAllFromProvider`) */
packages: PackageData[];
/** Other packages from this provider that didn't match filters (shown in secondary group) */
otherPackages?: PackageData[];
/** Secondary list below the primary one — same-provider-more or nearby-verified. Suppressed when `showAllFromProvider` is true. */
secondaryList?: SecondaryList;
/** Currently selected package ID */
selectedPackageId: string | null;
/** Callback when a package is selected */
onSelectPackage: (id: string) => void;
/** Callback when "Make Arrangement" is clicked (opens ArrangementDialog) */
/** Callback when a primary-list package is selected (or cleared via mobile back) */
onSelectPackage: (id: string | null) => void;
/** Callback when "Make Arrangement" / "Make an enquiry" is clicked */
onArrange: () => void;
/** Callback when the provider card is clicked (opens provider profile popup) */
/** Callback when the "Compare" button on the PackageDetail panel is clicked */
onCompare?: () => void;
/** Callback when a nearby-verified package card is clicked (route change to that provider) */
onNearbyPackageClick?: (id: string) => void;
/**
* Callback when "See all N packages from [Provider]" is clicked.
* Expected to route to the same PackagesStep with `showAllFromProvider` set.
* Only used when secondaryList.kind === 'same-provider-more' and list length > 3.
*/
onSeeAllPackages?: () => void;
/** Callback when the provider card is clicked (future: opens provider profile) */
onProviderClick?: () => void;
/** Callback for the Back button */
onBack: () => void;
/**
* When true, renders the "All packages from [Provider]" variant:
* flat list, no grouping, no secondary list, no "Matching your preferences" heading.
* Caller passes the full package list in `packages`.
*/
showAllFromProvider?: boolean;
/** Validation error */
error?: string;
/** Whether the arrange action is loading */
@@ -75,23 +124,60 @@ export interface PackagesStepProps {
sx?: SxProps<Theme>;
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
/** Accent bar + label — used for both "Matching your preferences" and "Other packages from [X]". */
function GroupHeading({
label,
emphasis = 'primary',
}: {
label: string;
emphasis?: 'primary' | 'secondary';
}) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>
<Box
sx={{
width: 3,
height: 20,
borderRadius: 1,
bgcolor: emphasis === 'primary' ? 'primary.main' : 'text.secondary',
flexShrink: 0,
}}
/>
<Typography
variant="body2"
sx={{
fontWeight: 600,
color: emphasis === 'primary' ? 'text.primary' : 'text.secondary',
}}
>
{label}
</Typography>
</Box>
);
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Step 3 — Package selection page for the FA arrangement wizard.
* Package selection step — tier-aware, unified page component.
*
* List + Detail split layout. Left panel shows the selected provider
* (compact) and selectable package cards. Right panel shows the full
* detail breakdown of the selected package with "Make Arrangement" CTA.
* Handles all three provider tiers (verified, tier3, tier2) via the
* `providerTier` prop. Header copy, CTA label, price disclaimer, and
* itemised-unavailable state are derived from tier.
*
* Packages are split into two groups:
* - **Matching your preferences**: packages that matched the user's filters
* from the providers step
* - **Other packages from [Provider]**: remaining packages outside those
* filters, shown below a divider for passive discovery
* Left column layout varies by `secondaryList`:
* - `same-provider-more` (verified): primary "Matching your preferences"
* list + "Other packages from [Provider]" list. If >3 other packages,
* 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).
*
* Selecting a package reveals its detail. Clicking "Make Arrangement"
* on the detail panel triggers the ArrangementDialog (D-E).
* When `showAllFromProvider` is true, renders a flat "All packages from
* [Provider]" list with no grouping and no secondary list. The caller
* preserves `selectedPackageId` across this navigation.
*
* Pure presentation component — props in, callbacks out.
*
@@ -99,191 +185,265 @@ export interface PackagesStepProps {
*/
export const PackagesStep: React.FC<PackagesStepProps> = ({
provider,
providerTier,
packages,
otherPackages = [],
secondaryList,
selectedPackageId,
onSelectPackage,
onArrange,
onCompare,
onNearbyPackageClick,
onSeeAllPackages,
onProviderClick,
onBack,
showAllFromProvider = false,
error,
loading = false,
navigation,
isPrePlanning = false,
sx,
}) => {
const allPackages = [...packages, ...otherPackages];
const selectedPackage = allPackages.find((p) => p.id === selectedPackageId);
const hasOtherPackages = otherPackages.length > 0;
const copy = TIER_COPY[providerTier];
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
const subheading = isPrePlanning
? 'Compare packages to find what suits your wishes. Nothing is committed until you confirm.'
: 'Each package includes a set of services. You can customise your selections in the next steps.';
// Mobile drill-in: when a package is selected on mobile, swap the list view
// for the detail view. Back button clears selection to return to the list.
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const mobileShowDetail = isMobile && selectedPackageId != null;
useEffect(() => {
if (mobileShowDetail) window.scrollTo({ top: 0, behavior: 'auto' });
}, [mobileShowDetail]);
const handleLayoutBack = mobileShowDetail ? () => onSelectPackage(null) : onBack;
const layoutBackLabel = mobileShowDetail ? 'Back to packages' : 'Back';
// Secondary list suppressed in "show all" mode.
const activeSecondaryList = showAllFromProvider ? undefined : secondaryList;
const hasSecondary = Boolean(activeSecondaryList);
// For same-provider-more, show top N inline; surface "See all" when over limit.
const sameProviderPackages =
activeSecondaryList?.kind === 'same-provider-more' ? activeSecondaryList.packages : [];
const sameProviderOverflow = sameProviderPackages.length > SAME_PROVIDER_INLINE_LIMIT;
const sameProviderVisible = sameProviderOverflow
? sameProviderPackages.slice(0, SAME_PROVIDER_INLINE_LIMIT)
: sameProviderPackages;
const heading = showAllFromProvider ? `All packages from ${provider.name}` : copy.heading;
const subheading = showAllFromProvider
? `Every package ${provider.name} offers, including those outside your preferences.`
: copy.subheading(isPrePlanning);
const primaryListAriaLabel = showAllFromProvider
? `All packages from ${provider.name}`
: 'Funeral packages';
return (
<WizardLayout
variant="list-detail"
navigation={navigation}
showBackLink
backLabel="Back"
onBack={onBack}
backLabel={layoutBackLabel}
onBack={handleLayoutBack}
sx={sx}
secondaryPanel={
selectedPackage ? (
<PackageDetail
name={selectedPackage.name}
price={selectedPackage.price}
sections={selectedPackage.sections}
total={selectedPackage.total}
extras={selectedPackage.extras}
terms={selectedPackage.terms}
onArrange={onArrange}
arrangeDisabled={loading}
/>
) : (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
minHeight: 300,
bgcolor: 'var(--fa-color-brand-50)',
borderRadius: 2,
p: 4,
}}
>
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center' }}>
Select a package to see what&apos;s included.
</Typography>
</Box>
)
}
>
{/* Provider compact card — clickable to open provider profile */}
<Box sx={{ mb: 3 }}>
<ProviderCardCompact
name={provider.name}
location={provider.location}
imageUrl={provider.imageUrl}
rating={provider.rating}
reviewCount={provider.reviewCount}
onClick={onProviderClick}
/>
</Box>
{/* Heading */}
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
Choose a funeral package
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{subheading}
</Typography>
{/* Error message */}
{error && (
<Typography
variant="body2"
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
role="alert"
>
{error}
</Typography>
)}
{/* ─── Matching packages ─── */}
{hasOtherPackages && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
mb: 2,
display: {
xs: mobileShowDetail ? 'block' : 'none',
md: 'block',
},
}}
>
<Box
sx={{
width: 3,
height: 20,
borderRadius: 1,
bgcolor: 'primary.main',
flexShrink: 0,
}}
/>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
Matching your preferences
</Typography>
</Box>
)}
<Box
role="radiogroup"
aria-label="Funeral packages"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{packages.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
))}
{packages.length === 0 && (
<Box sx={{ py: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
No packages match your current preferences.
</Typography>
</Box>
)}
</Box>
{/* ─── Other packages (passive discovery) ─── */}
{hasOtherPackages && (
<>
<Divider sx={{ mb: 2 }} />
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
mb: 2,
}}
>
{selectedPackage ? (
<PackageDetail
name={selectedPackage.name}
price={selectedPackage.price}
sections={selectedPackage.sections}
total={selectedPackage.total}
extras={selectedPackage.extras}
terms={selectedPackage.terms}
onArrange={onArrange}
onCompare={onCompare}
arrangeDisabled={loading}
arrangeLabel={copy.arrangeLabel}
priceDisclaimer={copy.priceDisclaimer}
itemizedUnavailable={copy.itemizedUnavailable}
/>
) : (
<Box
sx={{
width: 3,
height: 20,
borderRadius: 1,
bgcolor: 'text.secondary',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
minHeight: 300,
bgcolor: 'var(--fa-color-brand-50)',
borderRadius: 2,
p: 4,
}}
/>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary' }}>
Other packages from {provider.name}
</Typography>
</Box>
<Box
role="radiogroup"
aria-label={`Other packages from ${provider.name}`}
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3, opacity: 0.85 }}
>
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center' }}>
{copy.emptyDetailMessage}
</Typography>
</Box>
)}
</Box>
}
>
{/* List column — hidden on mobile when a package is selected (drill-in) */}
<Box
sx={{
display: {
xs: mobileShowDetail ? 'none' : 'block',
md: 'block',
},
}}
>
{/* Provider compact card */}
<Box sx={{ mb: 6 }}>
<ProviderCardCompact
name={provider.name}
location={provider.location}
imageUrl={provider.imageUrl}
rating={provider.rating}
reviewCount={provider.reviewCount}
onClick={onProviderClick}
/>
</Box>
{/* Heading + subheading */}
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
{heading}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 6 }}>
{subheading}
</Typography>
{/* Error */}
{error && (
<Typography
variant="body2"
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
role="alert"
>
{otherPackages.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
))}
</Box>
</>
)}
{error}
</Typography>
)}
{/* ─── Primary packages ─── */}
{/* Show "Matching your preferences" heading only when a secondary list follows */}
{hasSecondary && !showAllFromProvider && <GroupHeading label="Matching your preferences" />}
<Box
role="radiogroup"
aria-label={primaryListAriaLabel}
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 4 }}
>
{packages.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
))}
{packages.length === 0 && (
<Box sx={{ py: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
No packages match your current preferences.
</Typography>
</Box>
)}
</Box>
{/* ─── Secondary: same-provider-more ─── */}
{activeSecondaryList?.kind === 'same-provider-more' && sameProviderPackages.length > 0 && (
<>
<Divider sx={{ my: 8 }} />
<GroupHeading label={`Other packages from ${provider.name}`} emphasis="secondary" />
<Box
role="radiogroup"
aria-label={`Other packages from ${provider.name}`}
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
mb: sameProviderOverflow ? 2 : 3,
opacity: 0.85,
}}
>
{sameProviderVisible.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
))}
</Box>
{sameProviderOverflow && onSeeAllPackages && (
<Box sx={{ mb: 3 }}>
<Link
component="button"
type="button"
onClick={onSeeAllPackages}
underline="hover"
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
fontWeight: 600,
}}
>
See {sameProviderPackages.length - SAME_PROVIDER_INLINE_LIMIT} more packages from
this provider
<ArrowForwardIcon sx={{ fontSize: 16 }} aria-hidden />
</Link>
</Box>
)}
</>
)}
{/* ─── Secondary: nearby-verified ─── */}
{activeSecondaryList?.kind === 'nearby-verified' &&
activeSecondaryList.packages.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 />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
Similar packages from verified providers nearby
</Typography>
</Box>
<Box
aria-label="Similar packages from nearby verified providers"
sx={{ display: 'flex', flexDirection: 'column', 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}
/>
))}
</Box>
</>
)}
</Box>
</WizardLayout>
);
};

View File

@@ -0,0 +1,97 @@
import type { PackageSection } from '../../organisms/PackageDetail';
// ─── Tier ────────────────────────────────────────────────────────────────────
/**
* Provider tier — drives header copy, CTA label, disclaimer text, and
* whether the PackageDetail panel shows an itemised breakdown.
*
* - `verified`: Paid-listing provider. Full data, "Make Arrangement" CTA.
* - `tier3`: Unverified provider with itemised breakdown scraped from public info.
* - `tier2`: Unverified provider with total price only (no itemised breakdown).
*/
export type ProviderTier = 'verified' | 'tier3' | 'tier2';
// ─── Provider ────────────────────────────────────────────────────────────────
export interface PackagesStepProvider {
/** Provider name */
name: string;
/** Location */
location: string;
/** Hero image — typically only supplied for verified providers */
imageUrl?: string;
/** Rating */
rating?: number;
/** Review count */
reviewCount?: number;
}
// ─── Package data ────────────────────────────────────────────────────────────
/**
* Package data for the selection list.
*
* For `tier2` providers, callers should pass `sections: []` (and optionally
* omit `total`); the detail panel switches to "Itemised Pricing Unavailable"
* automatically based on the `providerTier` prop.
*/
export interface PackageData {
/** Unique package ID */
id: string;
/** Package display name */
name: string;
/** Package price in dollars */
price: number;
/** Short description shown on the option card */
description?: string;
/** Line-item sections for the detail panel (empty for tier2) */
sections: PackageSection[];
/** Total price shown between main sections and extras */
total?: number;
/** Extra-cost items shown after the total */
extras?: PackageSection;
/** Terms and conditions */
terms?: string;
}
/** A package offered by a nearby verified provider (promoted on unverified pages). */
export interface NearbyVerifiedPackage {
/** Unique ID */
id: string;
/** Package name */
packageName: string;
/** Package price in dollars */
price: number;
/** Provider name */
providerName: string;
/** Provider location */
location: string;
/** Provider rating */
rating?: number;
/** Number of reviews */
reviewCount?: number;
}
// ─── Secondary list ──────────────────────────────────────────────────────────
/**
* Discriminated union for the second list below the primary packages.
*
* - `same-provider-more`: Other packages from the same (verified) provider.
* 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.
*/
export type SecondaryList =
| {
kind: 'same-provider-more';
packages: PackageData[];
}
| {
kind: 'nearby-verified';
packages: NearbyVerifiedPackage[];
};

View File

@@ -1,206 +0,0 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { UnverifiedPackageT2 } from './UnverifiedPackageT2';
import type {
UnverifiedPackageT2Data,
UnverifiedPackageT2Provider,
NearbyVerifiedPackage,
} from './UnverifiedPackageT2';
import { Navigation } from '../../organisms/Navigation';
import Box from '@mui/material/Box';
// ─── Helpers ─────────────────────────────────────────────────────────────────
const FALogo = () => (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/brandlogo/logo-full.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
/>
<Box
component="img"
src="/brandlogo/logo-short.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
/>
</Box>
);
const nav = (
<Navigation
logo={<FALogo />}
items={[
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Log in', href: '/login' },
]}
/>
);
const mockProvider: UnverifiedPackageT2Provider = {
name: 'H.Parsons Funeral Directors',
location: 'Wentworth, NSW',
rating: 4.6,
reviewCount: 7,
};
const mockPackages: UnverifiedPackageT2Data[] = [
{
id: 'everyday',
name: 'Everyday Funeral Package',
price: 2700,
description:
'A funeral service at a chapel or church with a funeral procession, including commonly selected options.',
},
{
id: 'deluxe',
name: 'Deluxe Funeral Package',
price: 4900,
description: 'A comprehensive package with premium inclusions and expanded service options.',
},
{
id: 'catholic',
name: 'Catholic Service',
price: 3200,
description:
'Tailored for Catholic funeral traditions including a Requiem Mass and graveside prayers.',
},
];
const nearbyVerifiedPackages: NearbyVerifiedPackage[] = [
{
id: 'rankins-standard',
packageName: 'Standard Cremation Package',
price: 2450,
providerName: 'Rankins Funerals',
location: 'Warrawong, NSW',
rating: 4.8,
reviewCount: 23,
},
{
id: 'easy-essential',
packageName: 'Essential Funeral Service',
price: 1950,
providerName: 'Easy Funerals',
location: 'Sydney, NSW',
rating: 4.5,
reviewCount: 42,
},
{
id: 'killick-classic',
packageName: 'Classic Farewell Package',
price: 3100,
providerName: 'Killick Family Funerals',
location: 'Shellharbour, NSW',
rating: 4.9,
reviewCount: 15,
},
];
// ─── Meta ────────────────────────────────────────────────────────────────────
const meta: Meta<typeof UnverifiedPackageT2> = {
title: 'Pages/UnverifiedPackageT2',
component: UnverifiedPackageT2,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof UnverifiedPackageT2>;
// ─── Interactive (default) ──────────────────────────────────────────────────
/** Select a package to see the "Itemised Pricing Unavailable" detail panel */
export const Default: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<UnverifiedPackageT2
provider={mockProvider}
packages={mockPackages}
nearbyPackages={nearbyVerifiedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Make an enquiry')}
onNearbyPackageClick={(id) => alert(`View nearby package: ${id}`)}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── With selection ─────────────────────────────────────────────────────────
/** Package selected — detail panel shows price + unavailable notice */
export const WithSelection: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>('everyday');
return (
<UnverifiedPackageT2
provider={mockProvider}
packages={mockPackages}
nearbyPackages={nearbyVerifiedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Make an enquiry')}
onNearbyPackageClick={(id) => alert(`View nearby package: ${id}`)}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── No nearby packages ────────────────────────────────────────────────────
/** Only this provider's packages — no nearby verified section */
export const NoNearbyPackages: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<UnverifiedPackageT2
provider={mockProvider}
packages={mockPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Make an enquiry')}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── Validation error ───────────────────────────────────────────────────────
/** Error shown when no package selected */
export const WithError: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<UnverifiedPackageT2
provider={mockProvider}
packages={mockPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => {}}
onBack={() => alert('Back')}
error="Please choose a package to continue."
navigation={nav}
/>
);
},
};

View File

@@ -1,318 +0,0 @@
import React from 'react';
import Box from '@mui/material/Box';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout';
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
import { ServiceOption } from '../../molecules/ServiceOption';
import { PackageDetail } from '../../organisms/PackageDetail';
import { Typography } from '../../atoms/Typography';
import { Card } from '../../atoms/Card';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Provider summary for the compact card */
export interface UnverifiedPackageT2Provider {
/** Provider name */
name: string;
/** Location */
location: string;
/** Image URL */
imageUrl?: string;
/** Rating */
rating?: number;
/** Review count */
reviewCount?: number;
}
/** Package data — price only, no itemised breakdown */
export interface UnverifiedPackageT2Data {
/** Unique package ID */
id: string;
/** Package display name */
name: string;
/** Package price in dollars */
price: number;
/** Short description */
description?: string;
}
/** A similar package from a nearby verified provider */
export interface NearbyVerifiedPackage {
/** Unique ID */
id: string;
/** Package name */
packageName: string;
/** Package price in dollars */
price: number;
/** Provider name */
providerName: string;
/** Provider location */
location: string;
/** Provider rating */
rating?: number;
/** Number of reviews */
reviewCount?: number;
}
/** Props for the UnverifiedPackageT2 page component */
export interface UnverifiedPackageT2Props {
/** Provider summary shown at top of the list panel (no image — unverified provider) */
provider: UnverifiedPackageT2Provider;
/** Packages with price only (no itemised breakdown) */
packages: UnverifiedPackageT2Data[];
/** Similar packages from nearby verified providers */
nearbyPackages?: NearbyVerifiedPackage[];
/** Currently selected package ID */
selectedPackageId: string | null;
/** Callback when a package is selected */
onSelectPackage: (id: string) => void;
/** Callback when "Make an enquiry" is clicked */
onArrange: () => void;
/** Callback when a nearby verified package is clicked */
onNearbyPackageClick?: (id: string) => void;
/** Callback when the provider card is clicked */
onProviderClick?: () => void;
/** Callback for the Back button */
onBack: () => void;
/** Validation error */
error?: string;
/** Whether the enquiry action is loading */
loading?: boolean;
/** Navigation bar */
navigation?: React.ReactNode;
/** Whether this is a pre-planning flow */
isPrePlanning?: boolean;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* UnverifiedPackageT2 — Package selection page for Tier 2 unverified providers.
*
* Similar to T3 but the provider has only shared overall package prices,
* not itemised breakdowns. The detail panel shows an "Itemized Pricing
* Unavailable" notice instead of line items.
*
* Two sections:
* - **This provider's packages**: price-only, no breakdown available
* - **Similar packages from verified providers nearby**: promoted alternatives
*
* Pure presentation component — props in, callbacks out.
*/
export const UnverifiedPackageT2: React.FC<UnverifiedPackageT2Props> = ({
provider,
packages,
nearbyPackages = [],
selectedPackageId,
onSelectPackage,
onArrange,
onNearbyPackageClick,
onProviderClick,
onBack,
error,
loading = false,
navigation,
isPrePlanning = false,
sx,
}) => {
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
const hasNearbyPackages = nearbyPackages.length > 0;
const subheading = isPrePlanning
? 'Browse estimated packages to get a sense of what this provider offers. Nothing is committed.'
: 'These packages are based on publicly available information. Make an enquiry to confirm details and pricing.';
return (
<WizardLayout
variant="list-detail"
navigation={navigation}
showBackLink
backLabel="Back"
onBack={onBack}
sx={sx}
secondaryPanel={
selectedPackage ? (
<PackageDetail
name={selectedPackage.name}
price={selectedPackage.price}
sections={[]}
onArrange={onArrange}
arrangeDisabled={loading}
arrangeLabel="Make an enquiry"
priceDisclaimer="Prices are estimates based on publicly available information and may not reflect the provider's current pricing."
itemizedUnavailable
/>
) : (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
minHeight: 300,
bgcolor: 'var(--fa-color-brand-50)',
borderRadius: 2,
p: 4,
}}
>
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center' }}>
Select a package to see more details.
</Typography>
</Box>
)
}
>
{/* Provider compact card — no image for unverified */}
<Box sx={{ mb: 3 }}>
<ProviderCardCompact
name={provider.name}
location={provider.location}
rating={provider.rating}
reviewCount={provider.reviewCount}
onClick={onProviderClick}
/>
</Box>
{/* Heading */}
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
Explore available packages
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{subheading}
</Typography>
{/* Error message */}
{error && (
<Typography
variant="body2"
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
role="alert"
>
{error}
</Typography>
)}
{/* ─── Packages ─── */}
<Box
role="radiogroup"
aria-label="Funeral packages"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{packages.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
))}
{packages.length === 0 && (
<Box sx={{ py: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
No packages match your current preferences.
</Typography>
</Box>
)}
</Box>
{/* ─── Similar packages from nearby verified providers ─── */}
{hasNearbyPackages && (
<>
<Divider sx={{ mb: 2.5 }} />
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 2,
}}
>
<VerifiedOutlinedIcon sx={{ fontSize: 16, color: 'primary.main' }} aria-hidden />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
Similar packages from verified providers nearby
</Typography>
</Box>
<Box
aria-label="Similar packages from nearby verified providers"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{nearbyPackages.map((pkg) => (
<Card
key={pkg.id}
variant="outlined"
interactive={!!onNearbyPackageClick}
padding="none"
onClick={onNearbyPackageClick ? () => onNearbyPackageClick(pkg.id) : undefined}
sx={{ p: 'var(--fa-card-padding-compact)' }}
>
{/* Package name + price */}
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: 2,
mb: 1,
}}
>
<Typography variant="h6" component="span">
{pkg.packageName}
</Typography>
<Typography
variant="labelLg"
component="span"
color="primary"
sx={{ whiteSpace: 'nowrap' }}
>
${pkg.price.toLocaleString('en-AU')}
</Typography>
</Box>
{/* Provider info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary">
{pkg.providerName}
</Typography>
{pkg.rating != null && (
<>
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<StarRoundedIcon sx={{ fontSize: 14, color: 'warning.main' }} aria-hidden />
<Typography variant="caption" color="text.secondary">
{pkg.rating}
{pkg.reviewCount != null ? ` (${pkg.reviewCount})` : ''}
</Typography>
</>
)}
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<LocationOnOutlinedIcon
sx={{ fontSize: 14, color: 'text.secondary' }}
aria-hidden
/>
<Typography variant="caption" color="text.secondary">
{pkg.location}
</Typography>
</Box>
</Card>
))}
</Box>
</>
)}
</WizardLayout>
);
};
UnverifiedPackageT2.displayName = 'UnverifiedPackageT2';
export default UnverifiedPackageT2;

View File

@@ -1,2 +0,0 @@
export { default } from './UnverifiedPackageT2';
export * from './UnverifiedPackageT2';

View File

@@ -1,249 +0,0 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { UnverifiedPackageT3 } from './UnverifiedPackageT3';
import type {
UnverifiedPackageT3Data,
UnverifiedPackageT3Provider,
NearbyVerifiedPackage,
} from './UnverifiedPackageT3';
import { Navigation } from '../../organisms/Navigation';
import Box from '@mui/material/Box';
// ─── Helpers ─────────────────────────────────────────────────────────────────
const FALogo = () => (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/brandlogo/logo-full.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
/>
<Box
component="img"
src="/brandlogo/logo-short.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
/>
</Box>
);
const nav = (
<Navigation
logo={<FALogo />}
items={[
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Log in', href: '/login' },
]}
/>
);
const mockProvider: UnverifiedPackageT3Provider = {
name: 'H.Parsons Funeral Directors',
location: 'Wentworth, NSW',
rating: 4.6,
reviewCount: 7,
};
const matchedPackages: UnverifiedPackageT3Data[] = [
{
id: 'everyday',
name: 'Everyday Funeral Package',
price: 2700,
description:
'This package includes a funeral service at a chapel or a church with a funeral procession. It includes many of the most commonly selected funeral options.',
sections: [
{
heading: 'Essentials',
items: [
{ name: 'Accommodation', price: 500 },
{ name: 'Death registration certificate', price: 150 },
{ name: 'Doctor fee for Cremation', price: 150 },
{ name: 'NSW Government Levy - Cremation', price: 83 },
{ name: 'Professional Mortuary Care', price: 1200 },
{ name: 'Professional Service Fee', price: 1120 },
],
},
{
heading: 'Complimentary Items',
items: [
{ name: 'Dressing Fee', price: 0 },
{ name: 'Viewing Fee', price: 0 },
],
},
],
total: 2700,
extras: {
heading: 'Extras',
items: [
{ name: 'Allowance for Flowers', price: 150, isAllowance: true },
{ name: 'Allowance for Master of Ceremonies', price: 500, isAllowance: true },
{ name: 'After Business Hours Service Surcharge', price: 150 },
{ name: 'After Hours Prayers', price: 1920 },
{ name: 'Coffin Bearing by Funeral Directors', price: 1500 },
{ name: 'Digital Recording', price: 500 },
],
},
terms:
'This package includes a funeral service at a chapel or a church with a funeral procession. Pricing may vary based on additional selections.',
},
];
const nearbyVerifiedPackages: NearbyVerifiedPackage[] = [
{
id: 'rankins-standard',
packageName: 'Standard Cremation Package',
price: 2450,
providerName: 'Rankins Funerals',
location: 'Warrawong, NSW',
rating: 4.8,
reviewCount: 23,
},
{
id: 'easy-essential',
packageName: 'Essential Funeral Service',
price: 1950,
providerName: 'Easy Funerals',
location: 'Sydney, NSW',
rating: 4.5,
reviewCount: 42,
},
{
id: 'killick-classic',
packageName: 'Classic Farewell Package',
price: 3100,
providerName: 'Killick Family Funerals',
location: 'Shellharbour, NSW',
rating: 4.9,
reviewCount: 15,
},
];
// ─── Meta ────────────────────────────────────────────────────────────────────
const meta: Meta<typeof UnverifiedPackageT3> = {
title: 'Pages/UnverifiedPackageT3',
component: UnverifiedPackageT3,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof UnverifiedPackageT3>;
// ─── Interactive (default) ──────────────────────────────────────────────────
/** Matched + other packages — select a package, see detail, click Make Arrangement */
export const Default: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<UnverifiedPackageT3
provider={mockProvider}
packages={matchedPackages}
nearbyPackages={nearbyVerifiedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── With selection ─────────────────────────────────────────────────────────
/** Package already selected — detail panel visible */
export const WithSelection: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>('everyday');
return (
<UnverifiedPackageT3
provider={mockProvider}
packages={matchedPackages}
nearbyPackages={nearbyVerifiedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── No other packages (all match) ─────────────────────────────────────────
/** No nearby verified packages — only this provider's packages */
export const NoNearbyPackages: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<UnverifiedPackageT3
provider={mockProvider}
packages={matchedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── Pre-planning ───────────────────────────────────────────────────────────
/** Pre-planning flow — softer copy */
export const PrePlanning: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<UnverifiedPackageT3
provider={mockProvider}
packages={matchedPackages}
nearbyPackages={nearbyVerifiedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => alert('Open ArrangementDialog')}
onProviderClick={() => alert('Open provider profile')}
onBack={() => alert('Back')}
navigation={nav}
isPrePlanning
/>
);
},
};
// ─── Validation error ───────────────────────────────────────────────────────
/** Error shown when no package selected */
export const WithError: Story = {
render: () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<UnverifiedPackageT3
provider={mockProvider}
packages={matchedPackages}
selectedPackageId={selectedId}
onSelectPackage={setSelectedId}
onArrange={() => {}}
onBack={() => alert('Back')}
error="Please choose a package to continue."
navigation={nav}
/>
);
},
};

View File

@@ -1,333 +0,0 @@
import React from 'react';
import Box from '@mui/material/Box';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout';
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
import { ServiceOption } from '../../molecules/ServiceOption';
import { PackageDetail } from '../../organisms/PackageDetail';
import type { PackageSection } from '../../organisms/PackageDetail';
import { Typography } from '../../atoms/Typography';
import { Card } from '../../atoms/Card';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Provider summary for the compact card */
export interface UnverifiedPackageT3Provider {
/** Provider name */
name: string;
/** Location */
location: string;
/** Image URL */
imageUrl?: string;
/** Rating */
rating?: number;
/** Review count */
reviewCount?: number;
}
/** Package data for the selection list */
export interface UnverifiedPackageT3Data {
/** Unique package ID */
id: string;
/** Package display name */
name: string;
/** Package price in dollars */
price: number;
/** Short description */
description?: string;
/** Line item sections for the detail panel */
sections: PackageSection[];
/** Total price (may differ from base price with extras) */
total?: number;
/** Extra items section (after total) */
extras?: PackageSection;
/** Terms and conditions */
terms?: string;
}
/** A similar package from a nearby verified provider */
export interface NearbyVerifiedPackage {
/** Unique ID */
id: string;
/** Package name */
packageName: string;
/** Package price in dollars */
price: number;
/** Provider name */
providerName: string;
/** Provider location */
location: string;
/** Provider rating */
rating?: number;
/** Number of reviews */
reviewCount?: number;
}
/** Props for the UnverifiedPackageT3 page component */
export interface UnverifiedPackageT3Props {
/** Provider summary shown at top of the list panel (no image — unverified provider) */
provider: UnverifiedPackageT3Provider;
/** Packages matching the user's filters from the previous step */
packages: UnverifiedPackageT3Data[];
/** Similar packages from nearby verified providers */
nearbyPackages?: NearbyVerifiedPackage[];
/** Currently selected package ID */
selectedPackageId: string | null;
/** Callback when a package is selected */
onSelectPackage: (id: string) => void;
/** Callback when "Make Arrangement" is clicked (opens ArrangementDialog) */
onArrange: () => void;
/** Callback when a nearby verified package is clicked */
onNearbyPackageClick?: (id: string) => void;
/** Callback when the provider card is clicked (opens provider profile popup) */
onProviderClick?: () => void;
/** Callback for the Back button */
onBack: () => void;
/** Validation error */
error?: string;
/** Whether the arrange action is loading */
loading?: boolean;
/** Navigation bar */
navigation?: React.ReactNode;
/** Whether this is a pre-planning flow */
isPrePlanning?: boolean;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* UnverifiedPackageT3 — Package selection page for unverified (Tier 3) providers.
*
* List + Detail split layout. Left panel shows the selected provider
* (compact) and selectable package cards. Right panel shows the full
* detail breakdown of the selected package with "Make Arrangement" CTA.
*
* Two sections:
* - **This provider's packages**: estimated pricing from publicly available info
* - **Similar packages from verified providers nearby**: promoted alternatives
* with verified pricing, ratings, and location
*
* Selecting a package reveals its detail. Clicking "Make an enquiry"
* on the detail panel initiates contact with the unverified provider.
*
* Pure presentation component — props in, callbacks out.
*/
export const UnverifiedPackageT3: React.FC<UnverifiedPackageT3Props> = ({
provider,
packages,
nearbyPackages = [],
selectedPackageId,
onSelectPackage,
onArrange,
onNearbyPackageClick,
onProviderClick,
onBack,
error,
loading = false,
navigation,
isPrePlanning = false,
sx,
}) => {
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
const hasNearbyPackages = nearbyPackages.length > 0;
const subheading = isPrePlanning
? 'Browse estimated packages to get a sense of what this provider offers. Nothing is committed.'
: 'These packages are based on publicly available information. Make an enquiry to confirm details and pricing.';
return (
<WizardLayout
variant="list-detail"
navigation={navigation}
showBackLink
backLabel="Back"
onBack={onBack}
sx={sx}
secondaryPanel={
selectedPackage ? (
<PackageDetail
name={selectedPackage.name}
price={selectedPackage.price}
sections={selectedPackage.sections}
total={selectedPackage.total}
extras={selectedPackage.extras}
terms={selectedPackage.terms}
onArrange={onArrange}
arrangeDisabled={loading}
arrangeLabel="Make an enquiry"
priceDisclaimer="Prices are estimates based on publicly available information and may not reflect the provider's current pricing."
/>
) : (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
minHeight: 300,
bgcolor: 'var(--fa-color-brand-50)',
borderRadius: 2,
p: 4,
}}
>
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center' }}>
Select a package to see what&apos;s included.
</Typography>
</Box>
)
}
>
{/* Provider compact card — clickable to open provider profile */}
<Box sx={{ mb: 3 }}>
<ProviderCardCompact
name={provider.name}
location={provider.location}
rating={provider.rating}
reviewCount={provider.reviewCount}
onClick={onProviderClick}
/>
</Box>
{/* Heading */}
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
Explore available packages
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{subheading}
</Typography>
{/* Error message */}
{error && (
<Typography
variant="body2"
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
role="alert"
>
{error}
</Typography>
)}
{/* ─── Packages ─── */}
<Box
role="radiogroup"
aria-label="Funeral packages"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{packages.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
))}
{packages.length === 0 && (
<Box sx={{ py: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
No packages match your current preferences.
</Typography>
</Box>
)}
</Box>
{/* ─── Similar packages from nearby verified providers ─── */}
{hasNearbyPackages && (
<>
<Divider sx={{ mb: 2.5 }} />
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 2,
}}
>
<VerifiedOutlinedIcon sx={{ fontSize: 16, color: 'primary.main' }} aria-hidden />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
Similar packages from verified providers nearby
</Typography>
</Box>
<Box
aria-label="Similar packages from nearby verified providers"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{nearbyPackages.map((pkg) => (
<Card
key={pkg.id}
variant="outlined"
interactive={!!onNearbyPackageClick}
padding="none"
onClick={onNearbyPackageClick ? () => onNearbyPackageClick(pkg.id) : undefined}
sx={{ p: 'var(--fa-card-padding-compact)' }}
>
{/* Package name + price */}
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: 2,
mb: 1,
}}
>
<Typography variant="h6" component="span">
{pkg.packageName}
</Typography>
<Typography
variant="labelLg"
component="span"
color="primary"
sx={{ whiteSpace: 'nowrap' }}
>
${pkg.price.toLocaleString('en-AU')}
</Typography>
</Box>
{/* Provider info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary">
{pkg.providerName}
</Typography>
{pkg.rating != null && (
<>
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<StarRoundedIcon sx={{ fontSize: 14, color: 'warning.main' }} aria-hidden />
<Typography variant="caption" color="text.secondary">
{pkg.rating}
{pkg.reviewCount != null ? ` (${pkg.reviewCount})` : ''}
</Typography>
</>
)}
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<LocationOnOutlinedIcon
sx={{ fontSize: 14, color: 'text.secondary' }}
aria-hidden
/>
<Typography variant="caption" color="text.secondary">
{pkg.location}
</Typography>
</Box>
</Card>
))}
</Box>
</>
)}
</WizardLayout>
);
};
UnverifiedPackageT3.displayName = 'UnverifiedPackageT3';
export default UnverifiedPackageT3;

View File

@@ -1,2 +0,0 @@
export { default } from './UnverifiedPackageT3';
export * from './UnverifiedPackageT3';