diff --git a/brandassets/images/placeholder/hparsonsvenue.jpg b/brandassets/images/placeholder/hparsonsvenue.jpg new file mode 100644 index 0000000..cfbc111 Binary files /dev/null and b/brandassets/images/placeholder/hparsonsvenue.jpg differ 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';