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:
@@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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’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;
|
||||||
2
src/components/pages/UnverifiedProviderStep/index.ts
Normal file
2
src/components/pages/UnverifiedProviderStep/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './UnverifiedProviderStep';
|
||||||
|
export * from './UnverifiedProviderStep';
|
||||||
Reference in New Issue
Block a user