Add UnverifiedPackageT2/T3 pages, FuneralFinder pre-planning timeframe, PackageDetail variants
- 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>
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -7,3 +7,19 @@ tokens/export/
|
|||||||
.env.*
|
.env.*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.tgz
|
*.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
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Divider } from '../../atoms/Divider';
|
|||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type Status = 'immediate' | 'preplanning';
|
type Status = 'immediate' | 'preplanning';
|
||||||
|
type Timeframe = 'soon' | 'future';
|
||||||
type FuneralType =
|
type FuneralType =
|
||||||
| 'cremation-funeral'
|
| 'cremation-funeral'
|
||||||
| 'cremation-only'
|
| 'cremation-only'
|
||||||
@@ -26,6 +27,8 @@ type FuneralType =
|
|||||||
export interface FuneralFinderV3SearchParams {
|
export interface FuneralFinderV3SearchParams {
|
||||||
/** User's current situation */
|
/** User's current situation */
|
||||||
status: Status;
|
status: Status;
|
||||||
|
/** Pre-planning timeframe — only present when status is 'preplanning' */
|
||||||
|
timeframe?: Timeframe;
|
||||||
/** Type of funeral selected (defaults to show-all if not chosen) */
|
/** Type of funeral selected (defaults to show-all if not chosen) */
|
||||||
funeralType: FuneralType;
|
funeralType: FuneralType;
|
||||||
/** Suburb or postcode */
|
/** 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 }[] = [
|
const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [
|
||||||
{ value: 'cremation-funeral', label: 'Cremation with funeral' },
|
{ value: 'cremation-funeral', label: 'Cremation with funeral' },
|
||||||
{
|
{
|
||||||
@@ -150,23 +158,26 @@ const StatusCard = React.forwardRef<
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
display: 'block',
|
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',
|
color: selected ? 'var(--fa-color-text-brand, #B0610F)' : 'text.primary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
{description && (
|
||||||
variant="caption"
|
<Typography
|
||||||
component="span"
|
variant="caption"
|
||||||
sx={{
|
component="span"
|
||||||
display: 'block',
|
sx={{
|
||||||
color: 'text.secondary',
|
display: 'block',
|
||||||
lineHeight: 1.4,
|
color: 'text.secondary',
|
||||||
}}
|
lineHeight: 1.4,
|
||||||
>
|
}}
|
||||||
{description}
|
>
|
||||||
</Typography>
|
{description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
));
|
));
|
||||||
StatusCard.displayName = 'StatusCard';
|
StatusCard.displayName = 'StatusCard';
|
||||||
@@ -195,9 +206,9 @@ const fieldBaseSx = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fieldInputStyles = {
|
const fieldInputStyles = {
|
||||||
py: '14px',
|
py: '12px',
|
||||||
px: 2,
|
px: 2,
|
||||||
fontSize: '1rem',
|
fontSize: '0.9375rem',
|
||||||
fontFamily: 'var(--fa-font-family-body)',
|
fontFamily: 'var(--fa-font-family-body)',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -256,10 +267,12 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
|
|
||||||
// ─── State ───────────────────────────────────────────────
|
// ─── State ───────────────────────────────────────────────
|
||||||
const [status, setStatus] = React.useState<Status | ''>('immediate');
|
const [status, setStatus] = React.useState<Status | ''>('immediate');
|
||||||
|
const [timeframe, setTimeframe] = React.useState<Timeframe | ''>('');
|
||||||
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
|
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
|
||||||
const [location, setLocation] = React.useState('');
|
const [location, setLocation] = React.useState('');
|
||||||
const [errors, setErrors] = React.useState<{
|
const [errors, setErrors] = React.useState<{
|
||||||
status?: boolean;
|
status?: boolean;
|
||||||
|
timeframe?: boolean;
|
||||||
location?: boolean;
|
location?: boolean;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
@@ -290,6 +303,8 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
}
|
}
|
||||||
}, [location, errors.location]);
|
}, [location, errors.location]);
|
||||||
|
|
||||||
|
const isPrePlanning = status === 'preplanning';
|
||||||
|
|
||||||
// ─── Radiogroup keyboard nav (WAI-ARIA pattern) ──────────
|
// ─── Radiogroup keyboard nav (WAI-ARIA pattern) ──────────
|
||||||
const activeStatusIndex = status ? STATUS_OPTIONS.findIndex((o) => o.key === status) : 0;
|
const activeStatusIndex = status ? STATUS_OPTIONS.findIndex((o) => o.key === status) : 0;
|
||||||
|
|
||||||
@@ -305,10 +320,18 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
: Math.max(current - 1, 0);
|
: Math.max(current - 1, 0);
|
||||||
if (next !== current) {
|
if (next !== current) {
|
||||||
cardRefs.current[next]?.focus();
|
cardRefs.current[next]?.focus();
|
||||||
setStatus(STATUS_OPTIONS[next].key);
|
const newStatus = STATUS_OPTIONS[next].key;
|
||||||
|
setStatus(newStatus);
|
||||||
|
if (newStatus !== 'preplanning') setTimeframe('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStatusClick = (key: Status) => {
|
||||||
|
setStatus(key);
|
||||||
|
if (key !== 'preplanning') setTimeframe('');
|
||||||
|
if (errors.timeframe) setErrors((prev) => ({ ...prev, timeframe: false }));
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Handlers ────────────────────────────────────────────
|
// ─── Handlers ────────────────────────────────────────────
|
||||||
const handleFuneralType = (e: SelectChangeEvent<string>) => {
|
const handleFuneralType = (e: SelectChangeEvent<string>) => {
|
||||||
setFuneralType(e.target.value as FuneralType);
|
setFuneralType(e.target.value as FuneralType);
|
||||||
@@ -323,6 +346,14 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (status === 'preplanning' && !timeframe) {
|
||||||
|
setErrors({ timeframe: true });
|
||||||
|
statusSectionRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (location.trim().length < 3) {
|
if (location.trim().length < 3) {
|
||||||
setErrors({ location: true });
|
setErrors({ location: true });
|
||||||
locationSectionRef.current?.scrollIntoView({
|
locationSectionRef.current?.scrollIntoView({
|
||||||
@@ -335,6 +366,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
setErrors({});
|
setErrors({});
|
||||||
onSearch?.({
|
onSearch?.({
|
||||||
status,
|
status,
|
||||||
|
...(status === 'preplanning' && timeframe ? { timeframe } : {}),
|
||||||
funeralType: funeralType || 'show-all',
|
funeralType: funeralType || 'show-all',
|
||||||
location: location.trim(),
|
location: location.trim(),
|
||||||
});
|
});
|
||||||
@@ -355,7 +387,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
py: { xs: 4, sm: 5 },
|
py: { xs: 4, sm: 5 },
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 4,
|
gap: { xs: 3, sm: 4 },
|
||||||
},
|
},
|
||||||
...(Array.isArray(sx) ? sx : [sx]),
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
]}
|
]}
|
||||||
@@ -368,6 +400,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
sx={{
|
sx={{
|
||||||
fontFamily: 'var(--fa-font-family-display)',
|
fontFamily: 'var(--fa-font-family-display)',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
fontSize: { xs: '1.25rem', sm: '1.5rem' },
|
||||||
mb: subheading ? 1 : 0,
|
mb: subheading ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -404,7 +437,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
title={opt.title}
|
title={opt.title}
|
||||||
description={opt.description}
|
description={opt.description}
|
||||||
selected={status === opt.key}
|
selected={status === opt.key}
|
||||||
onClick={() => setStatus(opt.key)}
|
onClick={() => handleStatusClick(opt.key)}
|
||||||
tabIndex={i === activeStatusIndex ? 0 : -1}
|
tabIndex={i === activeStatusIndex ? 0 : -1}
|
||||||
onKeyDown={handleStatusKeyDown}
|
onKeyDown={handleStatusKeyDown}
|
||||||
/>
|
/>
|
||||||
@@ -425,6 +458,63 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* ── Timeframe follow-up (pre-planning only) ────────── */}
|
||||||
|
{isPrePlanning && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 3,
|
||||||
|
overflow: 'hidden',
|
||||||
|
animation: 'fadeSlideIn 200ms ease-out',
|
||||||
|
'@keyframes fadeSlideIn': {
|
||||||
|
'0%': { opacity: 0, transform: 'translateY(-8px)' },
|
||||||
|
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SectionLabel id={`${id}-timeframe`}>How Soon Might You Need This?</SectionLabel>
|
||||||
|
<Box
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby={`${id}-timeframe`}
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
|
||||||
|
gap: 2,
|
||||||
|
mt: 2,
|
||||||
|
'& button': { fontSize: '0.8125rem' },
|
||||||
|
'& button .MuiTypography-root': { fontSize: '0.8125rem' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TIMEFRAME_OPTIONS.map((opt) => (
|
||||||
|
<StatusCard
|
||||||
|
key={opt.key}
|
||||||
|
title={opt.label}
|
||||||
|
description=""
|
||||||
|
selected={timeframe === opt.key}
|
||||||
|
onClick={() => {
|
||||||
|
setTimeframe(opt.key);
|
||||||
|
if (errors.timeframe) setErrors((prev) => ({ ...prev, timeframe: false }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box aria-live="polite" sx={{ textAlign: 'center' }}>
|
||||||
|
{errors.timeframe && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
role="alert"
|
||||||
|
sx={{
|
||||||
|
color: 'var(--fa-color-text-brand, #B0610F)',
|
||||||
|
display: 'block',
|
||||||
|
mt: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Please select a timeframe
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* ── Funeral Type ────────────────────────────────────── */}
|
{/* ── Funeral Type ────────────────────────────────────── */}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
import { Typography } from '../../atoms/Typography';
|
import { Typography } from '../../atoms/Typography';
|
||||||
import { Button } from '../../atoms/Button';
|
import { Button } from '../../atoms/Button';
|
||||||
@@ -50,6 +51,12 @@ export interface PackageDetailProps {
|
|||||||
arrangeDisabled?: boolean;
|
arrangeDisabled?: boolean;
|
||||||
/** Whether the compare button is in loading state */
|
/** Whether the compare button is in loading state */
|
||||||
compareLoading?: boolean;
|
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 */
|
/** MUI sx prop for the root element */
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
}
|
}
|
||||||
@@ -116,6 +123,9 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
|||||||
onCompare,
|
onCompare,
|
||||||
arrangeDisabled = false,
|
arrangeDisabled = false,
|
||||||
compareLoading = false,
|
compareLoading = false,
|
||||||
|
arrangeLabel = 'Make Arrangement',
|
||||||
|
priceDisclaimer,
|
||||||
|
itemizedUnavailable = false,
|
||||||
sx,
|
sx,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -159,6 +169,32 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
|||||||
${price.toLocaleString('en-AU')}
|
${price.toLocaleString('en-AU')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{/* Price disclaimer */}
|
||||||
|
{priceDisclaimer && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 1,
|
||||||
|
mt: 1.5,
|
||||||
|
px: 1.5,
|
||||||
|
py: 1,
|
||||||
|
bgcolor: 'var(--fa-color-surface-cool, #F5F7FA)',
|
||||||
|
borderRadius: 'var(--fa-border-radius-sm, 6px)',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon
|
||||||
|
sx={{ fontSize: 16, color: 'text.secondary', mt: '1px', flexShrink: 0 }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.4 }}>
|
||||||
|
{priceDisclaimer}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* CTA buttons */}
|
{/* CTA buttons */}
|
||||||
<Box
|
<Box
|
||||||
sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, mt: 2.5 }}
|
sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, mt: 2.5 }}
|
||||||
@@ -170,7 +206,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
|||||||
disabled={arrangeDisabled}
|
disabled={arrangeDisabled}
|
||||||
onClick={onArrange}
|
onClick={onArrange}
|
||||||
>
|
>
|
||||||
Make Arrangement
|
{arrangeLabel}
|
||||||
</Button>
|
</Button>
|
||||||
{onCompare && (
|
{onCompare && (
|
||||||
<Button
|
<Button
|
||||||
@@ -189,24 +225,55 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
|||||||
|
|
||||||
{/* Package contents */}
|
{/* Package contents */}
|
||||||
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
|
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
|
||||||
{/* Main sections — included in the package price */}
|
{itemizedUnavailable ? (
|
||||||
{sections.map((section, idx) => (
|
<Box
|
||||||
<Box key={section.heading} sx={{ mb: idx < sections.length - 1 ? 3 : 0 }}>
|
sx={{
|
||||||
<SectionBlock section={section} />
|
display: 'flex',
|
||||||
</Box>
|
flexDirection: 'column',
|
||||||
))}
|
alignItems: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
{/* Total — separates included content from extras */}
|
bgcolor: 'var(--fa-color-surface-warm, #FEF9F5)',
|
||||||
{total != null && <LineItem name="Total" price={total} variant="total" />}
|
borderRadius: 'var(--fa-border-radius-md, 8px)',
|
||||||
|
border: '1px solid',
|
||||||
{/* Extras — additional cost items after the total */}
|
borderColor: 'divider',
|
||||||
{extras && extras.items.length > 0 && (
|
px: 3,
|
||||||
<>
|
py: 4,
|
||||||
<Divider sx={{ my: 3 }} />
|
}}
|
||||||
<SectionBlock
|
>
|
||||||
section={extras}
|
<InfoOutlinedIcon
|
||||||
subtext="These items can be added to your package at additional cost."
|
sx={{ fontSize: 28, color: 'var(--fa-color-brand-500)', mb: 1.5 }}
|
||||||
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
|
<Typography variant="h6" component="p" sx={{ mb: 1 }}>
|
||||||
|
Itemised Pricing Unavailable
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 360 }}>
|
||||||
|
This provider has shared their overall package price, but has not provided a
|
||||||
|
detailed breakdown of what is included.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Main sections — included in the package price */}
|
||||||
|
{sections.map((section, idx) => (
|
||||||
|
<Box key={section.heading} sx={{ mb: idx < sections.length - 1 ? 3 : 0 }}>
|
||||||
|
<SectionBlock section={section} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Total — separates included content from extras */}
|
||||||
|
{total != null && <LineItem name="Total" price={total} variant="total" />}
|
||||||
|
|
||||||
|
{/* Extras — additional cost items after the total */}
|
||||||
|
{extras && extras.items.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
<SectionBlock
|
||||||
|
section={extras}
|
||||||
|
subtext="These items can be added to your package at additional cost."
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -271,13 +271,14 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
|||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
|
width: '100%',
|
||||||
px: 2,
|
px: 2,
|
||||||
pt: 2,
|
pt: 2,
|
||||||
pb: 0,
|
pb: 0,
|
||||||
mb: { xs: -14, md: -18 },
|
mb: { xs: -14, md: -18 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ maxWidth: finderSlot ? 800 : 520, mx: 'auto' }}>
|
<Box sx={{ width: '100%', maxWidth: finderSlot ? 500 : 520, mx: 'auto' }}>
|
||||||
{finderSlot || (
|
{finderSlot || (
|
||||||
<FuneralFinderV3
|
<FuneralFinderV3
|
||||||
heading="Find your local providers"
|
heading="Find your local providers"
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
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 = () => (
|
||||||
|
<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 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<typeof UnverifiedPackageT2> = {
|
||||||
|
title: 'Pages/UnverifiedPackageT2',
|
||||||
|
component: UnverifiedPackageT2,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof UnverifiedPackageT2>;
|
||||||
|
|
||||||
|
// ─── Interactive (default) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Select a package to see the "Itemised Pricing Unavailable" detail panel */
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT2
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={mockPackages}
|
||||||
|
nearbyPackages={nearbyVerifiedPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => 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<string | null>('everyday');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT2
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={mockPackages}
|
||||||
|
nearbyPackages={nearbyVerifiedPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => 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<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT2
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={mockPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => 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<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT2
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={mockPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => {}}
|
||||||
|
onBack={() => alert('Back')}
|
||||||
|
error="Please choose a package to continue."
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
318
src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx
Normal file
318
src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx
Normal file
@@ -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<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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<UnverifiedPackageT2Props> = ({
|
||||||
|
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={[]}
|
||||||
|
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."
|
||||||
|
itemizedUnavailable
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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 more details.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Provider compact card — no image for unverified */}
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UnverifiedPackageT2.displayName = 'UnverifiedPackageT2';
|
||||||
|
export default UnverifiedPackageT2;
|
||||||
2
src/components/pages/UnverifiedPackageT2/index.ts
Normal file
2
src/components/pages/UnverifiedPackageT2/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './UnverifiedPackageT2';
|
||||||
|
export * from './UnverifiedPackageT2';
|
||||||
@@ -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 = () => (
|
||||||
|
<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 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<typeof UnverifiedPackageT3> = {
|
||||||
|
title: 'Pages/UnverifiedPackageT3',
|
||||||
|
component: UnverifiedPackageT3,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof UnverifiedPackageT3>;
|
||||||
|
|
||||||
|
// ─── Interactive (default) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Matched + other packages — select a package, see detail, click Make Arrangement */
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT3
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={matchedPackages}
|
||||||
|
nearbyPackages={nearbyVerifiedPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => 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<string | null>('everyday');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT3
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={matchedPackages}
|
||||||
|
nearbyPackages={nearbyVerifiedPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => 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<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT3
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={matchedPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => 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<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT3
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={matchedPackages}
|
||||||
|
nearbyPackages={nearbyVerifiedPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => 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<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnverifiedPackageT3
|
||||||
|
provider={mockProvider}
|
||||||
|
packages={matchedPackages}
|
||||||
|
selectedPackageId={selectedId}
|
||||||
|
onSelectPackage={setSelectedId}
|
||||||
|
onArrange={() => {}}
|
||||||
|
onBack={() => alert('Back')}
|
||||||
|
error="Please choose a package to continue."
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
333
src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx
Normal file
333
src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx
Normal file
@@ -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<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;
|
||||||
2
src/components/pages/UnverifiedPackageT3/index.ts
Normal file
2
src/components/pages/UnverifiedPackageT3/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './UnverifiedPackageT3';
|
||||||
|
export * from './UnverifiedPackageT3';
|
||||||
Reference in New Issue
Block a user