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