VenueDetailStep redesign + detail-toggles independent scroll

WizardLayout:
- detail-toggles now viewport-locked with independent panel scroll
- Left 55% scrollable, right 45% scrollable with divider border
- Back link rendered inside left panel (same as list-map)

VenueDetailStep redesign:
- Left: hero image, description, features (2-col grid with check icons),
  location map placeholder, address
- Right: venue name, icon meta rows (location, type, capacity),
  price + offset note, full-width Add Venue CTA, address, religion
  chips, service toggles
- MetaRow helper for consistent icon + text metadata display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 20:41:02 +11:00
parent 7f05f3812b
commit 289dc18025
2 changed files with 183 additions and 75 deletions

View File

@@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import HomeWorkOutlinedIcon from '@mui/icons-material/HomeWorkOutlined';
import PeopleOutlinedIcon from '@mui/icons-material/PeopleOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import type { SxProps, Theme } from '@mui/material/styles'; import type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout'; import { WizardLayout } from '../../templates/WizardLayout';
import { AddOnOption } from '../../molecules/AddOnOption'; import { AddOnOption } from '../../molecules/AddOnOption';
import { Card } from '../../atoms/Card';
import { Chip } from '../../atoms/Chip'; import { Chip } from '../../atoms/Chip';
import { Typography } from '../../atoms/Typography'; import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button'; import { Button } from '../../atoms/Button';
@@ -21,6 +24,7 @@ export interface VenueDetail {
venueType?: string; venueType?: string;
capacity?: number; capacity?: number;
price?: number; price?: number;
/** Explanation of how the price affects the plan total */
priceNote?: string; priceNote?: string;
description?: string; description?: string;
features?: string[]; features?: string[];
@@ -75,16 +79,31 @@ export interface VenueDetailStepProps {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
// ─── Helper ─────────────────────────────────────────────────────────────────
/** Compact icon + text row used for venue metadata */
const MetaRow: React.FC<{ icon: React.ReactNode; children: React.ReactNode }> = ({
icon,
children,
}) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ color: 'text.secondary', display: 'flex', fontSize: 18 }}>{icon}</Box>
<Typography variant="body2" color="text.secondary">
{children}
</Typography>
</Box>
);
// ─── Component ─────────────────────────────────────────────────────────────── // ─── Component ───────────────────────────────────────────────────────────────
/** /**
* Venue Detail page for the FA arrangement wizard. * Venue Detail page for the FA arrangement wizard.
* *
* Detail-toggles layout: venue image + description on the left, * Detail-toggles layout: scrollable left panel with image, description,
* venue info, price, CTA, and service toggles on the right. * features, location, and address. Sticky right panel with venue info,
* price, CTA, supported religions, and service toggles.
* *
* Reached by clicking a venue card on VenueStep. User reviews * Reached by clicking a venue card on VenueStep.
* the venue and clicks "Add Venue" to select it.
* *
* Pure presentation component — props in, callbacks out. * Pure presentation component — props in, callbacks out.
* *
@@ -126,41 +145,42 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
hideHelpBar={hideHelpBar} hideHelpBar={hideHelpBar}
sx={sx} sx={sx}
secondaryPanel={ secondaryPanel={
<Box sx={{ position: 'sticky', top: 24 }}> <Box>
{/* Venue name */} {/* ─── Venue name ─── */}
<Typography variant="h4" component="h1" sx={{ mb: 1 }}> <Typography variant="h4" component="h1" sx={{ mb: 1.5 }}>
{venue.name} {venue.name}
</Typography> </Typography>
{/* Location */} {/* ─── Meta: location, type, capacity ─── */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, mb: 3 }}>
<LocationOnOutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} aria-hidden /> <MetaRow icon={<LocationOnOutlinedIcon fontSize="inherit" />}>{venue.location}</MetaRow>
<Typography variant="body2" color="text.secondary"> {venue.venueType && (
{venue.location} <MetaRow icon={<HomeWorkOutlinedIcon fontSize="inherit" />}>
</Typography> {venue.venueType}
</MetaRow>
)}
{venue.capacity != null && (
<MetaRow icon={<PeopleOutlinedIcon fontSize="inherit" />}>
Seats up to {venue.capacity} guests
</MetaRow>
)}
</Box> </Box>
{/* Venue type */} {/* ─── Price ─── */}
{venue.venueType && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{venue.venueType}
</Typography>
)}
{/* Price */}
{venue.price != null && ( {venue.price != null && (
<Typography variant="h5" color="primary" sx={{ mb: 0.5 }}> <Box sx={{ mb: 3 }}>
<Typography variant="h5" color="primary">
${venue.price.toLocaleString('en-AU')} ${venue.price.toLocaleString('en-AU')}
</Typography> </Typography>
)}
{venue.priceNote && ( {venue.priceNote && (
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}> <Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{venue.priceNote} {venue.priceNote}
</Typography> </Typography>
)} )}
</Box>
)}
{/* Add Venue CTA */} {/* ─── Add Venue CTA ─── */}
<Box <Box
component="form" component="form"
noValidate noValidate
@@ -170,14 +190,7 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
if (!loading) onAddVenue(); if (!loading) onAddVenue();
}} }}
> >
<Button <Button type="submit" variant="contained" size="large" fullWidth loading={loading}>
type="submit"
variant="contained"
size="large"
fullWidth
loading={loading}
sx={{ mb: 2 }}
>
{isPrePlanning ? 'Select venue' : 'Add venue'} {isPrePlanning ? 'Select venue' : 'Add venue'}
</Button> </Button>
</Box> </Box>
@@ -188,42 +201,48 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
color="secondary" color="secondary"
fullWidth fullWidth
onClick={onSaveAndExit} onClick={onSaveAndExit}
sx={{ mb: 3 }} sx={{ mt: 1 }}
> >
Save and continue later Save and continue later
</Button> </Button>
)} )}
{/* Address */} <Divider sx={{ my: 3 }} />
{/* ─── Address ─── */}
{venue.address && ( {venue.address && (
<> <Box sx={{ mb: 3 }}>
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 0.5 }}> <Typography variant="labelSm" color="text.secondary" sx={{ mb: 0.5 }}>
Address Address
</Typography> </Typography>
<Typography variant="body2" sx={{ mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
{venue.address} <LocationOnOutlinedIcon
</Typography> sx={{ fontSize: 16, color: 'text.secondary', mt: 0.25 }}
</> aria-hidden
/>
<Typography variant="body2">{venue.address}</Typography>
</Box>
</Box>
)} )}
{/* Supported religions */} {/* ─── Supported religions / service styles ─── */}
{venue.religions && venue.religions.length > 0 && ( {venue.religions && venue.religions.length > 0 && (
<> <Box sx={{ mb: 3 }}>
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 1 }}> <Typography variant="labelSm" color="text.secondary" sx={{ mb: 1 }}>
Supported service styles Supported service styles
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 3 }}> <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{venue.religions.map((r) => ( {venue.religions.map((r) => (
<Chip key={r} label={r} size="small" /> <Chip key={r} label={r} size="small" />
))} ))}
</Box> </Box>
</> </Box>
)} )}
{/* Service toggles */} {/* ─── Service toggles ─── */}
{services.length > 0 && ( {services.length > 0 && (
<> <>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 3 }} />
<Typography variant="h5" sx={{ mb: 2 }}> <Typography variant="h5" sx={{ mb: 2 }}>
Venue services Venue services
</Typography> </Typography>
@@ -257,13 +276,15 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
</Box> </Box>
} }
> >
{/* Left panel: image + description + features */} {/* ═══════ LEFT PANEL: scrollable content ═══════ */}
{/* ─── Hero image ─── */}
<Box <Box
role="img" role="img"
aria-label={`Photo of ${venue.name}`} aria-label={`Photo of ${venue.name}`}
sx={{ sx={{
width: '100%', width: '100%',
height: { xs: 280, md: 400 }, height: { xs: 280, md: 420 },
borderRadius: 2, borderRadius: 2,
backgroundImage: `url(${venue.imageUrl})`, backgroundImage: `url(${venue.imageUrl})`,
backgroundSize: 'cover', backgroundSize: 'cover',
@@ -273,26 +294,69 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
}} }}
/> />
{/* ─── Description ─── */}
{venue.description && ( {venue.description && (
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body1" color="text.primary" sx={{ mb: 3, lineHeight: 1.7 }}>
{venue.description} {venue.description}
</Typography> </Typography>
)} )}
{/* Features */} {/* ─── Features ─── */}
{venue.features && venue.features.length > 0 && ( {venue.features && venue.features.length > 0 && (
<Box sx={{ mb: 3 }}> <>
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 1 }}> <Divider sx={{ my: 3 }} />
<Typography variant="h5" sx={{ mb: 2 }}>
Venue features Venue features
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> <Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)' },
gap: 1.5,
mb: 3,
}}
>
{venue.features.map((feature) => ( {venue.features.map((feature) => (
<Box key={feature} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box key={feature} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircleOutlineIcon sx={{ fontSize: 16, color: 'success.main' }} aria-hidden /> <CheckCircleOutlineIcon sx={{ fontSize: 18, color: 'success.main' }} aria-hidden />
<Typography variant="body2">{feature}</Typography> <Typography variant="body2">{feature}</Typography>
</Box> </Box>
))} ))}
</Box> </Box>
</>
)}
{/* ─── Location (map placeholder) ─── */}
<Divider sx={{ my: 3 }} />
<Typography variant="h5" sx={{ mb: 2 }}>
Location
</Typography>
<Card variant="outlined" padding="none" sx={{ mb: 3, overflow: 'hidden' }}>
<Box
sx={{
height: 200,
bgcolor: 'var(--fa-color-surface-cool)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography variant="body2" color="text.secondary">
Map coming soon
</Typography>
</Box>
</Card>
{/* ─── Address (left panel) ─── */}
{venue.address && (
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 3 }}>
<LocationOnOutlinedIcon
sx={{ fontSize: 18, color: 'text.secondary', mt: 0.25 }}
aria-hidden
/>
<Typography variant="body2" color="text.secondary">
{venue.address}
</Typography>
</Box> </Box>
)} )}
</WizardLayout> </WizardLayout>

View File

@@ -257,24 +257,68 @@ const GridSidebarLayout: React.FC<{
</Container> </Container>
); );
/** Detail + Toggles: two-column hero (image left / info right), full-width section below */ /** Detail + Toggles: scrollable left (image/desc) / sticky right (info/CTA) */
const DetailTogglesLayout: React.FC<{ const DetailTogglesLayout: React.FC<{
children: React.ReactNode; children: React.ReactNode;
secondaryPanel?: React.ReactNode; secondaryPanel?: React.ReactNode;
}> = ({ children, secondaryPanel }) => ( backLink?: React.ReactNode;
<Container maxWidth="lg" sx={{ flex: 1, py: 3 }}> }> = ({ children, secondaryPanel, backLink }) => (
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Left panel — scrollable content */}
<Box <Box
sx={{ sx={{
display: 'flex', width: { xs: '100%', md: '55%' },
gap: { xs: 0, md: 4 }, flexShrink: 0,
flexDirection: { xs: 'column', md: 'row' }, overflowY: 'auto',
mb: 4, px: { xs: 2, md: 4 },
pt: 0,
pb: 3,
scrollbarWidth: 'thin',
scrollbarColor: 'transparent transparent',
'&:hover': {
scrollbarColor: 'rgba(0,0,0,0.25) transparent',
},
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-thumb': {
background: 'transparent',
borderRadius: 3,
},
'&:hover::-webkit-scrollbar-thumb': {
background: 'rgba(0,0,0,0.25)',
},
}} }}
> >
<Box sx={{ width: { xs: '100%', md: '50%' } }}>{children}</Box> {backLink && <Box sx={{ pt: 1.5 }}>{backLink}</Box>}
<Box sx={{ width: { xs: '100%', md: '50%' } }}>{secondaryPanel}</Box> {children}
</Box>
{/* Right panel — sticky info */}
<Box
sx={{
display: { xs: 'none', md: 'block' },
width: { md: '45%' },
overflowY: 'auto',
px: 4,
py: 3,
borderLeft: '1px solid',
borderColor: 'divider',
scrollbarWidth: 'thin',
scrollbarColor: 'transparent transparent',
'&:hover': {
scrollbarColor: 'rgba(0,0,0,0.25) transparent',
},
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-thumb': {
background: 'transparent',
borderRadius: 3,
},
'&:hover::-webkit-scrollbar-thumb': {
background: 'rgba(0,0,0,0.25)',
},
}}
>
{secondaryPanel}
</Box>
</Box> </Box>
</Container>
); );
// ─── Variant map ───────────────────────────────────────────────────────────── // ─── Variant map ─────────────────────────────────────────────────────────────
@@ -343,8 +387,8 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
flexDirection: 'column', flexDirection: 'column',
minHeight: '100vh', minHeight: '100vh',
bgcolor: 'background.default', bgcolor: 'background.default',
// list-map: lock to viewport so only the left panel scrolls // list-map + detail-toggles: lock to viewport so panels scroll independently
...(variant === 'list-map' && { ...((variant === 'list-map' || variant === 'detail-toggles') && {
height: '100vh', height: '100vh',
overflow: 'hidden', overflow: 'hidden',
}), }),
@@ -358,8 +402,8 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
{/* Stepper + running total bar (grid-sidebar, detail-toggles only) */} {/* Stepper + running total bar (grid-sidebar, detail-toggles only) */}
{showStepper && <StepperBar stepper={progressStepper} total={runningTotal} />} {showStepper && <StepperBar stepper={progressStepper} total={runningTotal} />}
{/* Back link — inside left panel for list-map, above content for others */} {/* Back link — inside left panel for list-map/detail-toggles, above content for others */}
{showBackLink && variant !== 'list-map' && ( {showBackLink && variant !== 'list-map' && variant !== 'detail-toggles' && (
<Container <Container
maxWidth={variant === 'centered-form' ? 'sm' : 'lg'} maxWidth={variant === 'centered-form' ? 'sm' : 'lg'}
sx={{ pt: 2, px: { xs: 4, md: 3 } }} sx={{ pt: 2, px: { xs: 4, md: 3 } }}
@@ -376,7 +420,7 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
<LayoutComponent <LayoutComponent
secondaryPanel={secondaryPanel} secondaryPanel={secondaryPanel}
backLink={ backLink={
showBackLink && variant === 'list-map' ? ( showBackLink && (variant === 'list-map' || variant === 'detail-toggles') ? (
<Box sx={{ pt: 1.5 }}> <Box sx={{ pt: 1.5 }}>
<BackLink label={backLabel} onClick={onBack} /> <BackLink label={backLabel} onClick={onBack} />
</Box> </Box>