diff --git a/.gitignore b/.gitignore
index 9bc046e..6293d49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,19 @@ tokens/export/
.env.*
.DS_Store
*.tgz
+
+# Claude / Playwright artifacts
+.playwright-mcp/
+.claude/
+
+# Build logs
+build-storybook.log
+
+# Working docs (not for sharing)
+documentation/
+DESIGN.md
+venue-services-snapshot.md
+temp-db/
+
+# Root-level screenshots
+/*.png
diff --git a/src/components/organisms/FuneralFinder/FuneralFinderV3.tsx b/src/components/organisms/FuneralFinder/FuneralFinderV3.tsx
index c42ea8a..2052bb8 100644
--- a/src/components/organisms/FuneralFinder/FuneralFinderV3.tsx
+++ b/src/components/organisms/FuneralFinder/FuneralFinderV3.tsx
@@ -14,6 +14,7 @@ import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
type Status = 'immediate' | 'preplanning';
+type Timeframe = 'soon' | 'future';
type FuneralType =
| 'cremation-funeral'
| 'cremation-only'
@@ -26,6 +27,8 @@ type FuneralType =
export interface FuneralFinderV3SearchParams {
/** User's current situation */
status: Status;
+ /** Pre-planning timeframe — only present when status is 'preplanning' */
+ timeframe?: Timeframe;
/** Type of funeral selected (defaults to show-all if not chosen) */
funeralType: FuneralType;
/** Suburb or postcode */
@@ -61,6 +64,11 @@ const STATUS_OPTIONS: { key: Status; title: string; description: string }[] = [
},
];
+const TIMEFRAME_OPTIONS: { key: Timeframe; label: string }[] = [
+ { key: 'soon', label: 'In the coming weeks or months' },
+ { key: 'future', label: 'No set timeframe, I\u2019m planning ahead' },
+];
+
const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [
{ value: 'cremation-funeral', label: 'Cremation with funeral' },
{
@@ -150,23 +158,26 @@ const StatusCard = React.forwardRef<
sx={{
fontWeight: 600,
display: 'block',
- mb: 0.75,
+ fontSize: { xs: '0.875rem', sm: '1rem' },
+ mb: description ? 0.75 : 0,
color: selected ? 'var(--fa-color-text-brand, #B0610F)' : 'text.primary',
}}
>
{title}
-
- {description}
-
+ {description && (
+
+ {description}
+
+ )}
));
StatusCard.displayName = 'StatusCard';
@@ -195,9 +206,9 @@ const fieldBaseSx = {
};
const fieldInputStyles = {
- py: '14px',
+ py: '12px',
px: 2,
- fontSize: '1rem',
+ fontSize: '0.9375rem',
fontFamily: 'var(--fa-font-family-body)',
};
@@ -256,10 +267,12 @@ export const FuneralFinderV3 = React.forwardRef('immediate');
+ const [timeframe, setTimeframe] = React.useState('');
const [funeralType, setFuneralType] = React.useState('');
const [location, setLocation] = React.useState('');
const [errors, setErrors] = React.useState<{
status?: boolean;
+ timeframe?: boolean;
location?: boolean;
}>({});
@@ -290,6 +303,8 @@ export const FuneralFinderV3 = React.forwardRef o.key === status) : 0;
@@ -305,10 +320,18 @@ export const FuneralFinderV3 = React.forwardRef {
+ setStatus(key);
+ if (key !== 'preplanning') setTimeframe('');
+ if (errors.timeframe) setErrors((prev) => ({ ...prev, timeframe: false }));
+ };
+
// ─── Handlers ────────────────────────────────────────────
const handleFuneralType = (e: SelectChangeEvent) => {
setFuneralType(e.target.value as FuneralType);
@@ -323,6 +346,14 @@ export const FuneralFinderV3 = React.forwardRef
@@ -404,7 +437,7 @@ export const FuneralFinderV3 = React.forwardRef setStatus(opt.key)}
+ onClick={() => handleStatusClick(opt.key)}
tabIndex={i === activeStatusIndex ? 0 : -1}
onKeyDown={handleStatusKeyDown}
/>
@@ -425,6 +458,63 @@ export const FuneralFinderV3 = React.forwardRef
)}
+
+ {/* ── Timeframe follow-up (pre-planning only) ────────── */}
+ {isPrePlanning && (
+
+ How Soon Might You Need This?
+
+ {TIMEFRAME_OPTIONS.map((opt) => (
+ {
+ setTimeframe(opt.key);
+ if (errors.timeframe) setErrors((prev) => ({ ...prev, timeframe: false }));
+ }}
+ />
+ ))}
+
+
+ {errors.timeframe && (
+
+ Please select a timeframe
+
+ )}
+
+
+ )}
{/* ── Funeral Type ────────────────────────────────────── */}
diff --git a/src/components/organisms/PackageDetail/PackageDetail.tsx b/src/components/organisms/PackageDetail/PackageDetail.tsx
index 7737e54..a3e99db 100644
--- a/src/components/organisms/PackageDetail/PackageDetail.tsx
+++ b/src/components/organisms/PackageDetail/PackageDetail.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import Box from '@mui/material/Box';
+import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import type { SxProps, Theme } from '@mui/material/styles';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
@@ -50,6 +51,12 @@ export interface PackageDetailProps {
arrangeDisabled?: boolean;
/** Whether the compare button is in loading state */
compareLoading?: boolean;
+ /** Custom label for the arrange CTA button (default: "Make Arrangement") */
+ arrangeLabel?: string;
+ /** Disclaimer shown below the price (e.g. for unverified/estimated pricing) */
+ priceDisclaimer?: string;
+ /** When true, replaces the itemised breakdown with an "Itemised Pricing Unavailable" notice */
+ itemizedUnavailable?: boolean;
/** MUI sx prop for the root element */
sx?: SxProps;
}
@@ -116,6 +123,9 @@ export const PackageDetail = React.forwardRef
+ {/* Price disclaimer */}
+ {priceDisclaimer && (
+
+
+
+ {priceDisclaimer}
+
+
+ )}
+
{/* CTA buttons */}
- Make Arrangement
+ {arrangeLabel}
{onCompare && (
+ ) : (
+ <>
+ {/* Main sections — included in the package price */}
+ {sections.map((section, idx) => (
+
+
+
+ ))}
+
+ {/* Total — separates included content from extras */}
+ {total != null && }
+
+ {/* Extras — additional cost items after the total */}
+ {extras && extras.items.length > 0 && (
+ <>
+
+
+ >
+ )}
>
)}
diff --git a/src/components/pages/HomePage/HomePage.tsx b/src/components/pages/HomePage/HomePage.tsx
index 8195f15..79876e7 100644
--- a/src/components/pages/HomePage/HomePage.tsx
+++ b/src/components/pages/HomePage/HomePage.tsx
@@ -271,13 +271,14 @@ export const HomePage = React.forwardRef(
sx={{
position: 'relative',
zIndex: 2,
+ width: '100%',
px: 2,
pt: 2,
pb: 0,
mb: { xs: -14, md: -18 },
}}
>
-
+
{finderSlot || (
(
+
+
+
+
+);
+
+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
new file mode 100644
index 0000000..eaeb4e2
--- /dev/null
+++ b/src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx
@@ -0,0 +1,318 @@
+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
new file mode 100644
index 0000000..6c3f37b
--- /dev/null
+++ b/src/components/pages/UnverifiedPackageT2/index.ts
@@ -0,0 +1,2 @@
+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
new file mode 100644
index 0000000..5404bcd
--- /dev/null
+++ b/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.stories.tsx
@@ -0,0 +1,249 @@
+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
new file mode 100644
index 0000000..2798839
--- /dev/null
+++ b/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx
@@ -0,0 +1,333 @@
+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
new file mode 100644
index 0000000..c2d2b84
--- /dev/null
+++ b/src/components/pages/UnverifiedPackageT3/index.ts
@@ -0,0 +1,2 @@
+export { default } from './UnverifiedPackageT3';
+export * from './UnverifiedPackageT3';