From e67872cb6a281846b57f656595890ff8abbe0b02 Mon Sep 17 00:00:00 2001 From: Richie Date: Mon, 20 Apr 2026 12:45:57 +1000 Subject: [PATCH] Unify PackagesStep across tiers + polish pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../images/placeholder/hparsonsvenue.jpg | Bin 0 -> 53249 bytes docs/memory/component-registry.md | 3 +- docs/memory/session-log.md | 108 +++- .../NearbyPackageCard.stories.tsx | 93 +++ .../NearbyPackageCard/NearbyPackageCard.tsx | 106 ++++ .../molecules/NearbyPackageCard/index.ts | 1 + .../PackageDetail/PackageDetail.stories.tsx | 175 ------ .../organisms/PackageDetail/PackageDetail.tsx | 13 +- .../PackagesStep/PackagesStep.stories.tsx | 315 ++++++++-- .../pages/PackagesStep/PackagesStep.tsx | 580 +++++++++++------- src/components/pages/PackagesStep/types.ts | 97 +++ .../UnverifiedPackageT2.stories.tsx | 206 ------- .../UnverifiedPackageT2.tsx | 318 ---------- .../pages/UnverifiedPackageT2/index.ts | 2 - .../UnverifiedPackageT3.stories.tsx | 249 -------- .../UnverifiedPackageT3.tsx | 333 ---------- .../pages/UnverifiedPackageT3/index.ts | 2 - 17 files changed, 1048 insertions(+), 1553 deletions(-) create mode 100644 brandassets/images/placeholder/hparsonsvenue.jpg create mode 100644 src/components/molecules/NearbyPackageCard/NearbyPackageCard.stories.tsx create mode 100644 src/components/molecules/NearbyPackageCard/NearbyPackageCard.tsx create mode 100644 src/components/molecules/NearbyPackageCard/index.ts create mode 100644 src/components/pages/PackagesStep/types.ts delete mode 100644 src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.stories.tsx delete mode 100644 src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx delete mode 100644 src/components/pages/UnverifiedPackageT2/index.ts delete mode 100644 src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.stories.tsx delete mode 100644 src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx delete mode 100644 src/components/pages/UnverifiedPackageT3/index.ts diff --git a/brandassets/images/placeholder/hparsonsvenue.jpg b/brandassets/images/placeholder/hparsonsvenue.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cfbc111e92afd835bd9780bc66f13a4589faea1e GIT binary patch literal 53249 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
~kxuYj5}(q;WAd&}
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

literal 0
HcmV?d00001

diff --git a/docs/memory/component-registry.md b/docs/memory/component-registry.md
index b1f0306..751dd0a 100644
--- a/docs/memory/component-registry.md
+++ b/docs/memory/component-registry.md
@@ -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". `
` 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. | diff --git a/docs/memory/session-log.md b/docs/memory/session-log.md index 9c89211..876a1cf 100644 --- a/docs/memory/session-log.md +++ b/docs/memory/session-log.md @@ -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 120–160px 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. --- diff --git a/src/components/molecules/NearbyPackageCard/NearbyPackageCard.stories.tsx b/src/components/molecules/NearbyPackageCard/NearbyPackageCard.stories.tsx new file mode 100644 index 0000000..bf56822 --- /dev/null +++ b/src/components/molecules/NearbyPackageCard/NearbyPackageCard.stories.tsx @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { NearbyPackageCard } from './NearbyPackageCard'; + +const meta: Meta = { + title: 'Molecules/NearbyPackageCard', + component: NearbyPackageCard, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** 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: () => ( + + {}} + /> + {}} + /> + {}} + /> + + ), +}; diff --git a/src/components/molecules/NearbyPackageCard/NearbyPackageCard.tsx b/src/components/molecules/NearbyPackageCard/NearbyPackageCard.tsx new file mode 100644 index 0000000..bc4cc80 --- /dev/null +++ b/src/components/molecules/NearbyPackageCard/NearbyPackageCard.tsx @@ -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; +} + +// ─── 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( + ({ packageName, price, providerName, location, rating, reviewCount, onClick, sx }, ref) => { + return ( + + {/* Package name + price */} + + + {packageName} + + + ${price.toLocaleString('en-AU')} + + + + {/* Provider info */} + + + {providerName} + + {rating != null && ( + <> + + · + + + + {rating} + {reviewCount != null ? ` (${reviewCount})` : ''} + + + )} + + · + + + + {location} + + + + ); + }, +); + +NearbyPackageCard.displayName = 'NearbyPackageCard'; +export default NearbyPackageCard; diff --git a/src/components/molecules/NearbyPackageCard/index.ts b/src/components/molecules/NearbyPackageCard/index.ts new file mode 100644 index 0000000..a8e76da --- /dev/null +++ b/src/components/molecules/NearbyPackageCard/index.ts @@ -0,0 +1 @@ +export { NearbyPackageCard, type NearbyPackageCardProps } from './NearbyPackageCard'; diff --git a/src/components/organisms/PackageDetail/PackageDetail.stories.tsx b/src/components/organisms/PackageDetail/PackageDetail.stories.tsx index 13dc7b5..6cd1391 100644 --- a/src/components/organisms/PackageDetail/PackageDetail.stories.tsx +++ b/src/components/organisms/PackageDetail/PackageDetail.stories.tsx @@ -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 = () => ( - -); - const meta: Meta = { 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) => ( - - - - ), - ], - 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 ( - - } - items={[ - { label: 'Provider Portal', href: '/provider-portal' }, - { label: 'FAQ', href: '/faq' }, - { label: 'Contact Us', href: '/contact' }, - { label: 'Log in', href: '/login' }, - ]} - /> - - - {/* Left column */} - - - - - Select a package - - - - - {/* Funeral type filter */} - - {funeralTypes.map((type) => ( - setActiveFilter(type)} - size="small" - /> - ))} - - - - Packages - - - - {packages.map((pkg) => ( - setSelectedPkg(pkg.id)} - maxDescriptionLines={2} - /> - ))} - - - - {/* Right column: package detail */} - - 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} - /> - - - - ); - }, -}; diff --git a/src/components/organisms/PackageDetail/PackageDetail.tsx b/src/components/organisms/PackageDetail/PackageDetail.tsx index 535a185..3211399 100644 --- a/src/components/organisms/PackageDetail/PackageDetail.tsx +++ b/src/components/organisms/PackageDetail/PackageDetail.tsx @@ -141,6 +141,7 @@ export const PackageDetail = React.forwardRef - + {priceDisclaimer} diff --git a/src/components/pages/PackagesStep/PackagesStep.stories.tsx b/src/components/pages/PackagesStep/PackagesStep.stories.tsx index 4eae043..1f23494 100644 --- a/src/components/pages/PackagesStep/PackagesStep.stories.tsx +++ b/src/components/pages/PackagesStep/PackagesStep.stories.tsx @@ -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 today’s 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 = { @@ -161,45 +274,24 @@ const meta: Meta = { export default meta; type Story = StoryObj; -// ─── Interactive (default) ────────────────────────────────────────────────── +// ─── Verified ──────────────────────────────────────────────────────────────── -/** Matched + other packages — select a package, see detail, click Make Arrangement */ -export const Default: Story = { - render: () => { - const [selectedId, setSelectedId] = useState(null); - - return ( - 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('everyday'); return ( 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('everyday'); + + return ( + 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('everyday'); + const allPackages = [...matchedPackages, ...manyOtherPackages]; + + return ( + 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('everyday'); + + return ( + 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('t2-standard'); + + return ( + 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(null); return ( 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(null); + + return ( + 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 ( 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(null); return ( 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 = { + 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; } +// ─── 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 ( + + + + {label} + + + ); +} + // ─── 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 = ({ 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 ( - ) : ( - - - Select a package to see what's included. - - - ) - } - > - {/* Provider compact card — clickable to open provider profile */} - - - - - {/* Heading */} - - Choose a funeral package - - - {subheading} - - - {/* Error message */} - {error && ( - - {error} - - )} - - {/* ─── Matching packages ─── */} - {hasOtherPackages && ( - - - Matching your preferences - - - )} - - - {packages.map((pkg) => ( - onSelectPackage(pkg.id)} - /> - ))} - - {packages.length === 0 && ( - - - No packages match your current preferences. - - - )} - - - {/* ─── Other packages (passive discovery) ─── */} - {hasOtherPackages && ( - <> - - + {selectedPackage ? ( + + ) : ( - - Other packages from {provider.name} - - - + + {copy.emptyDetailMessage} + + + )} + + } + > + {/* List column — hidden on mobile when a package is selected (drill-in) */} + + {/* Provider compact card */} + + + + + {/* Heading + subheading */} + + {heading} + + + {subheading} + + + {/* Error */} + {error && ( + - {otherPackages.map((pkg) => ( - onSelectPackage(pkg.id)} - /> - ))} - - - )} + {error} + + )} + + {/* ─── Primary packages ─── */} + {/* Show "Matching your preferences" heading only when a secondary list follows */} + {hasSecondary && !showAllFromProvider && } + + + {packages.map((pkg) => ( + onSelectPackage(pkg.id)} + /> + ))} + + {packages.length === 0 && ( + + + No packages match your current preferences. + + + )} + + + {/* ─── Secondary: same-provider-more ─── */} + {activeSecondaryList?.kind === 'same-provider-more' && sameProviderPackages.length > 0 && ( + <> + + + + {sameProviderVisible.map((pkg) => ( + onSelectPackage(pkg.id)} + /> + ))} + + + {sameProviderOverflow && onSeeAllPackages && ( + + + See {sameProviderPackages.length - SAME_PROVIDER_INLINE_LIMIT} more packages from + this provider + + + + )} + + )} + + {/* ─── Secondary: nearby-verified ─── */} + {activeSecondaryList?.kind === 'nearby-verified' && + activeSecondaryList.packages.length > 0 && ( + <> + + + + + Similar packages from verified providers nearby + + + + {activeSecondaryList.packages.map((pkg) => ( + onNearbyPackageClick(pkg.id) : undefined} + /> + ))} + + + )} + ); }; diff --git a/src/components/pages/PackagesStep/types.ts b/src/components/pages/PackagesStep/types.ts new file mode 100644 index 0000000..b6b6a47 --- /dev/null +++ b/src/components/pages/PackagesStep/types.ts @@ -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[]; + }; diff --git a/src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.stories.tsx b/src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.stories.tsx deleted file mode 100644 index f9b267a..0000000 --- a/src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.stories.tsx +++ /dev/null @@ -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 = () => ( - - - - -); - -const nav = ( - } - 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 = { - title: 'Pages/UnverifiedPackageT2', - component: UnverifiedPackageT2, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - }, -}; - -export default meta; -type Story = StoryObj; - -// ─── Interactive (default) ────────────────────────────────────────────────── - -/** Select a package to see the "Itemised Pricing Unavailable" detail panel */ -export const Default: Story = { - render: () => { - const [selectedId, setSelectedId] = useState(null); - - return ( - 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('everyday'); - - return ( - 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(null); - - return ( - 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(null); - - return ( - {}} - onBack={() => alert('Back')} - error="Please choose a package to continue." - navigation={nav} - /> - ); - }, -}; diff --git a/src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx b/src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx deleted file mode 100644 index eaeb4e2..0000000 --- a/src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx +++ /dev/null @@ -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; -} - -// ─── 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 = ({ - 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 ( - - ) : ( - - - Select a package to see more details. - - - ) - } - > - {/* Provider compact card — no image for unverified */} - - - - - {/* Heading */} - - Explore available packages - - - {subheading} - - - {/* Error message */} - {error && ( - - {error} - - )} - - {/* ─── Packages ─── */} - - {packages.map((pkg) => ( - onSelectPackage(pkg.id)} - /> - ))} - - {packages.length === 0 && ( - - - No packages match your current preferences. - - - )} - - - {/* ─── Similar packages from nearby verified providers ─── */} - {hasNearbyPackages && ( - <> - - - - - Similar packages from verified providers nearby - - - - {nearbyPackages.map((pkg) => ( - onNearbyPackageClick(pkg.id) : undefined} - sx={{ p: 'var(--fa-card-padding-compact)' }} - > - {/* Package name + price */} - - - {pkg.packageName} - - - ${pkg.price.toLocaleString('en-AU')} - - - - {/* Provider info */} - - - {pkg.providerName} - - {pkg.rating != null && ( - <> - - · - - - - {pkg.rating} - {pkg.reviewCount != null ? ` (${pkg.reviewCount})` : ''} - - - )} - - · - - - - {pkg.location} - - - - ))} - - - )} - - ); -}; - -UnverifiedPackageT2.displayName = 'UnverifiedPackageT2'; -export default UnverifiedPackageT2; diff --git a/src/components/pages/UnverifiedPackageT2/index.ts b/src/components/pages/UnverifiedPackageT2/index.ts deleted file mode 100644 index 6c3f37b..0000000 --- a/src/components/pages/UnverifiedPackageT2/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './UnverifiedPackageT2'; -export * from './UnverifiedPackageT2'; diff --git a/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.stories.tsx b/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.stories.tsx deleted file mode 100644 index 5404bcd..0000000 --- a/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.stories.tsx +++ /dev/null @@ -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 = () => ( - - - - -); - -const nav = ( - } - 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 = { - title: 'Pages/UnverifiedPackageT3', - component: UnverifiedPackageT3, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - }, -}; - -export default meta; -type Story = StoryObj; - -// ─── Interactive (default) ────────────────────────────────────────────────── - -/** Matched + other packages — select a package, see detail, click Make Arrangement */ -export const Default: Story = { - render: () => { - const [selectedId, setSelectedId] = useState(null); - - return ( - 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('everyday'); - - return ( - 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(null); - - return ( - 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(null); - - return ( - 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(null); - - return ( - {}} - onBack={() => alert('Back')} - error="Please choose a package to continue." - navigation={nav} - /> - ); - }, -}; diff --git a/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx b/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx deleted file mode 100644 index 2798839..0000000 --- a/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx +++ /dev/null @@ -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; -} - -// ─── 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 = ({ - 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 ( - - ) : ( - - - Select a package to see what's included. - - - ) - } - > - {/* Provider compact card — clickable to open provider profile */} - - - - - {/* Heading */} - - Explore available packages - - - {subheading} - - - {/* Error message */} - {error && ( - - {error} - - )} - - {/* ─── Packages ─── */} - - {packages.map((pkg) => ( - onSelectPackage(pkg.id)} - /> - ))} - - {packages.length === 0 && ( - - - No packages match your current preferences. - - - )} - - - {/* ─── Similar packages from nearby verified providers ─── */} - {hasNearbyPackages && ( - <> - - - - - Similar packages from verified providers nearby - - - - {nearbyPackages.map((pkg) => ( - onNearbyPackageClick(pkg.id) : undefined} - sx={{ p: 'var(--fa-card-padding-compact)' }} - > - {/* Package name + price */} - - - {pkg.packageName} - - - ${pkg.price.toLocaleString('en-AU')} - - - - {/* Provider info */} - - - {pkg.providerName} - - {pkg.rating != null && ( - <> - - · - - - - {pkg.rating} - {pkg.reviewCount != null ? ` (${pkg.reviewCount})` : ''} - - - )} - - · - - - - {pkg.location} - - - - ))} - - - )} - - ); -}; - -UnverifiedPackageT3.displayName = 'UnverifiedPackageT3'; -export default UnverifiedPackageT3; diff --git a/src/components/pages/UnverifiedPackageT3/index.ts b/src/components/pages/UnverifiedPackageT3/index.ts deleted file mode 100644 index c2d2b84..0000000 --- a/src/components/pages/UnverifiedPackageT3/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './UnverifiedPackageT3'; -export * from './UnverifiedPackageT3';