New UnverifiedProviderStep page for scraped provider listings

Page shown when user selects an unverified provider from ProvidersStep.
Displays whatever data we have, offers enquiry CTA, recommends verified
alternatives.

Sections (all conditional based on available data):
- Provider header: ProviderCardCompact + "Listing" info badge
- What we know: definition list grid (pricing, services, area)
- Enquiry CTA: warm-bg card with "Make an Enquiry" button
- Verified recommendations: 2-col grid of ProviderCards

4 story variants: Default (full data), MinimalData, NoData, NoRecommendations.
Uses centered-form WizardLayout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 17:54:33 +11:00
parent a127a50dea
commit abb7f46a33
3 changed files with 391 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
import type { Meta, StoryObj } from '@storybook/react';
import { UnverifiedProviderStep } from './UnverifiedProviderStep';
import type { RecommendedProvider, ProviderDetail } from './UnverifiedProviderStep';
import { Navigation } from '../../organisms/Navigation';
import Box from '@mui/material/Box';
// ─── Helpers ─────────────────────────────────────────────────────────────────
const FALogo = () => (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/brandlogo/logo-full.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
/>
<Box
component="img"
src="/brandlogo/logo-short.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
/>
</Box>
);
const nav = (
<Navigation
logo={<FALogo />}
items={[
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Log in', href: '/login' },
]}
/>
);
const knownDetails: ProviderDetail[] = [
{ label: 'Estimated pricing', value: 'From approximately $750' },
{ label: 'Services', value: 'Cremation, Service & Cremation, Burial' },
{ label: 'Service area', value: 'Wollongong & surrounding suburbs' },
];
const minimalDetails: ProviderDetail[] = [
{ label: 'Estimated pricing', value: 'From approximately $750' },
];
const recommendedProviders: RecommendedProvider[] = [
{
id: 'parsons',
name: 'H.Parsons Funeral Directors',
location: 'Wentworth, NSW',
imageUrl: 'https://placehold.co/600x200/E8E0D6/8B6F47?text=H.Parsons',
logoUrl: 'https://placehold.co/64x64/FEF9F5/BA834E?text=HP',
rating: 4.6,
reviewCount: 7,
startingPrice: 900,
},
{
id: 'rankins',
name: 'Rankins Funeral Services',
location: 'Wollongong, NSW',
imageUrl: 'https://placehold.co/600x200/D7E1E2/4C5B6B?text=Rankins',
logoUrl: 'https://placehold.co/64x64/F2F5F6/4C5B6B?text=R',
rating: 4.8,
reviewCount: 23,
startingPrice: 1200,
},
];
// ─── Meta ────────────────────────────────────────────────────────────────────
const meta: Meta<typeof UnverifiedProviderStep> = {
title: 'Pages/UnverifiedProviderStep',
component: UnverifiedProviderStep,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof UnverifiedProviderStep>;
// ─── With known details + recommendations ───────────────────────────────────
/** Full data — pricing, services, and verified alternatives */
export const Default: Story = {
args: {
providerName: 'Wollongong City Funerals',
providerLocation: 'Wollongong, NSW',
providerRating: 4.2,
providerReviewCount: 15,
details: knownDetails,
recommendations: recommendedProviders,
recommendationContext: 'near Wollongong',
onEnquire: () => alert('Enquiry submitted'),
onSelectRecommendation: (id) => alert(`Navigate to provider: ${id}`),
onBack: () => alert('Back to providers'),
navigation: nav,
},
};
// ─── Minimal data ───────────────────────────────────────────────────────────
/** Only rough pricing available — no services, no reviews */
export const MinimalData: Story = {
args: {
providerName: 'Botanical Funerals',
providerLocation: 'Newtown, NSW',
details: minimalDetails,
recommendations: recommendedProviders,
recommendationContext: 'near Newtown',
onEnquire: () => alert('Enquiry submitted'),
onSelectRecommendation: (id) => alert(`Navigate to provider: ${id}`),
onBack: () => alert('Back to providers'),
navigation: nav,
},
};
// ─── No data at all ─────────────────────────────────────────────────────────
/** No scraped data — just the name and location */
export const NoData: Story = {
args: {
providerName: 'Smith & Sons Funerals',
providerLocation: 'Penrith, NSW',
recommendations: recommendedProviders,
recommendationContext: 'near Penrith',
onEnquire: () => alert('Enquiry submitted'),
onSelectRecommendation: (id) => alert(`Navigate to provider: ${id}`),
onBack: () => alert('Back to providers'),
navigation: nav,
},
};
// ─── No recommendations ─────────────────────────────────────────────────────
/** No verified alternatives available for this area */
export const NoRecommendations: Story = {
args: {
providerName: 'Wollongong City Funerals',
providerLocation: 'Wollongong, NSW',
providerRating: 4.2,
providerReviewCount: 15,
details: knownDetails,
onEnquire: () => alert('Enquiry submitted'),
onBack: () => alert('Back to providers'),
navigation: nav,
},
};

View File

@@ -0,0 +1,239 @@
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 { WizardLayout } from '../../templates/WizardLayout';
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
import { ProviderCard } from '../../molecules/ProviderCard';
import { Badge } from '../../atoms/Badge';
import { Button } from '../../atoms/Button';
import { Card } from '../../atoms/Card';
import { Typography } from '../../atoms/Typography';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** A piece of known information about the unverified provider */
export interface ProviderDetail {
/** Label (e.g. "Estimated pricing") */
label: string;
/** Value (e.g. "From approximately $750") */
value: string;
}
/** A verified provider recommendation */
export interface RecommendedProvider {
id: string;
name: string;
location: string;
imageUrl?: string;
logoUrl?: string;
rating?: number;
reviewCount?: number;
startingPrice?: number;
}
/** Props for the UnverifiedProviderStep page */
export interface UnverifiedProviderStepProps {
/** Provider display name */
providerName: string;
/** Provider location */
providerLocation: string;
/** Average rating (if available from reviews) */
providerRating?: number;
/** Number of reviews */
providerReviewCount?: number;
/** Known details — only sections with data are rendered */
details?: ProviderDetail[];
/** Callback when "Make an Enquiry" is clicked */
onEnquire: () => void;
/** Whether the enquiry is being submitted */
enquiryLoading?: boolean;
/** Verified provider recommendations matching the user's search */
recommendations?: RecommendedProvider[];
/** Label for the recommendations section (e.g. "near Wollongong") */
recommendationContext?: string;
/** Callback when a recommended provider is clicked */
onSelectRecommendation?: (id: string) => void;
/** Callback for back navigation */
onBack: () => void;
/** Navigation bar */
navigation?: React.ReactNode;
/** Progress stepper */
progressStepper?: React.ReactNode;
/** Running total widget */
runningTotal?: React.ReactNode;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Unverified provider detail page for the FA arrangement wizard.
*
* Shown when a user selects an unverified (scraped) provider from
* the ProvidersStep. Displays whatever information we have, offers
* an enquiry CTA, and recommends verified alternatives.
*
* Uses centered-form layout — single column, focused experience.
* No package selection, no arrangement flow.
*
* Pure presentation component — props in, callbacks out.
*/
export const UnverifiedProviderStep: React.FC<UnverifiedProviderStepProps> = ({
providerName,
providerLocation,
providerRating,
providerReviewCount,
details = [],
onEnquire,
enquiryLoading = false,
recommendations = [],
recommendationContext,
onSelectRecommendation,
onBack,
navigation,
progressStepper,
runningTotal,
sx,
}) => {
return (
<WizardLayout
variant="centered-form"
navigation={navigation}
progressStepper={progressStepper}
runningTotal={runningTotal}
showBackLink
backLabel="Back to providers"
onBack={onBack}
sx={sx}
>
{/* ── Provider header ── */}
<Box sx={{ pt: 2, mb: 3 }}>
<ProviderCardCompact
name={providerName}
location={providerLocation}
rating={providerRating}
reviewCount={providerReviewCount}
/>
<Box sx={{ mt: 1.5 }}>
<Badge variant="soft" color="default" size="medium" icon={<InfoOutlinedIcon />}>
Listing
</Badge>
</Box>
</Box>
{/* ── Available information ── */}
{details.length > 0 && (
<Box sx={{ mb: 3 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
What we know
</Typography>
<Box
component="dl"
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'auto 1fr' },
gap: 1.5,
m: 0,
'& dt': { color: 'text.secondary', m: 0 },
'& dd': { m: 0, fontWeight: 500 },
}}
>
{details.map((detail) => (
<React.Fragment key={detail.label}>
<Typography component="dt" variant="body2">
{detail.label}
</Typography>
<Typography component="dd" variant="body2">
{detail.value}
</Typography>
</React.Fragment>
))}
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
Based on publicly available information. Pricing and services may vary.
</Typography>
</Box>
)}
{/* ── Enquiry CTA ── */}
<Card
variant="elevated"
sx={{
bgcolor: 'var(--fa-color-surface-warm)',
p: { xs: 3, sm: 4 },
mb: 4,
textAlign: 'center',
}}
>
<Typography variant="h5" sx={{ mb: 1 }}>
Interested in {providerName}?
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 3, maxWidth: 420, mx: 'auto' }}
>
We&rsquo;ll pass your details along so they can reach out to you directly. No commitment
required.
</Typography>
<Button
variant="contained"
size="large"
onClick={onEnquire}
loading={enquiryLoading}
fullWidth
sx={{ maxWidth: 320, mx: 'auto' }}
>
Make an Enquiry
</Button>
</Card>
{/* ── Verified recommendations ── */}
{recommendations.length > 0 && (
<>
<Divider sx={{ mb: 3 }} />
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 0.5 }}>
Verified providers{recommendationContext ? ` ${recommendationContext}` : ''}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Full online arrangement with transparent pricing
</Typography>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)' },
gap: 2,
}}
>
{recommendations.map((provider) => (
<ProviderCard
key={provider.id}
name={provider.name}
location={provider.location}
verified
imageUrl={provider.imageUrl}
logoUrl={provider.logoUrl}
rating={provider.rating}
reviewCount={provider.reviewCount}
startingPrice={provider.startingPrice}
onClick={
onSelectRecommendation ? () => onSelectRecommendation(provider.id) : undefined
}
/>
))}
</Box>
</Box>
</>
)}
</WizardLayout>
);
};
UnverifiedProviderStep.displayName = 'UnverifiedProviderStep';
export default UnverifiedProviderStep;

View File

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