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 Box from '@mui/material/Box';
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 type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout';
import { AddOnOption } from '../../molecules/AddOnOption';
import { Card } from '../../atoms/Card';
import { Chip } from '../../atoms/Chip';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
@@ -21,6 +24,7 @@ export interface VenueDetail {
venueType?: string;
capacity?: number;
price?: number;
/** Explanation of how the price affects the plan total */
priceNote?: string;
description?: string;
features?: string[];
@@ -75,16 +79,31 @@ export interface VenueDetailStepProps {
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 ───────────────────────────────────────────────────────────────
/**
* Venue Detail page for the FA arrangement wizard.
*
* Detail-toggles layout: venue image + description on the left,
* venue info, price, CTA, and service toggles on the right.
* Detail-toggles layout: scrollable left panel with image, description,
* 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
* the venue and clicks "Add Venue" to select it.
* Reached by clicking a venue card on VenueStep.
*
* Pure presentation component — props in, callbacks out.
*
@@ -126,41 +145,42 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
hideHelpBar={hideHelpBar}
sx={sx}
secondaryPanel={
<Box sx={{ position: 'sticky', top: 24 }}>
{/* Venue name */}
<Typography variant="h4" component="h1" sx={{ mb: 1 }}>
<Box>
{/* ─── Venue name ─── */}
<Typography variant="h4" component="h1" sx={{ mb: 1.5 }}>
{venue.name}
</Typography>
{/* Location */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
<LocationOnOutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} aria-hidden />
<Typography variant="body2" color="text.secondary">
{venue.location}
</Typography>
{/* ─── Meta: location, type, capacity ─── */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, mb: 3 }}>
<MetaRow icon={<LocationOnOutlinedIcon fontSize="inherit" />}>{venue.location}</MetaRow>
{venue.venueType && (
<MetaRow icon={<HomeWorkOutlinedIcon fontSize="inherit" />}>
{venue.venueType}
</MetaRow>
)}
{venue.capacity != null && (
<MetaRow icon={<PeopleOutlinedIcon fontSize="inherit" />}>
Seats up to {venue.capacity} guests
</MetaRow>
)}
</Box>
{/* Venue type */}
{venue.venueType && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{venue.venueType}
</Typography>
)}
{/* Price */}
{/* ─── Price ─── */}
{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')}
</Typography>
)}
{venue.priceNote && (
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{venue.priceNote}
</Typography>
)}
</Box>
)}
{/* Add Venue CTA */}
{/* ─── Add Venue CTA ─── */}
<Box
component="form"
noValidate
@@ -170,14 +190,7 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
if (!loading) onAddVenue();
}}
>
<Button
type="submit"
variant="contained"
size="large"
fullWidth
loading={loading}
sx={{ mb: 2 }}
>
<Button type="submit" variant="contained" size="large" fullWidth loading={loading}>
{isPrePlanning ? 'Select venue' : 'Add venue'}
</Button>
</Box>
@@ -188,42 +201,48 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
color="secondary"
fullWidth
onClick={onSaveAndExit}
sx={{ mb: 3 }}
sx={{ mt: 1 }}
>
Save and continue later
</Button>
)}
{/* Address */}
<Divider sx={{ my: 3 }} />
{/* ─── Address ─── */}
{venue.address && (
<>
<Box sx={{ mb: 3 }}>
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 0.5 }}>
Address
</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>
{venue.address}
</Typography>
</>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<LocationOnOutlinedIcon
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 && (
<>
<Box sx={{ mb: 3 }}>
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 1 }}>
Supported service styles
</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) => (
<Chip key={r} label={r} size="small" />
))}
</Box>
</>
</Box>
)}
{/* Service toggles */}
{/* ─── Service toggles ─── */}
{services.length > 0 && (
<>
<Divider sx={{ my: 2 }} />
<Divider sx={{ my: 3 }} />
<Typography variant="h5" sx={{ mb: 2 }}>
Venue services
</Typography>
@@ -257,13 +276,15 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
</Box>
}
>
{/* Left panel: image + description + features */}
{/* ═══════ LEFT PANEL: scrollable content ═══════ */}
{/* ─── Hero image ─── */}
<Box
role="img"
aria-label={`Photo of ${venue.name}`}
sx={{
width: '100%',
height: { xs: 280, md: 400 },
height: { xs: 280, md: 420 },
borderRadius: 2,
backgroundImage: `url(${venue.imageUrl})`,
backgroundSize: 'cover',
@@ -273,26 +294,69 @@ export const VenueDetailStep: React.FC<VenueDetailStepProps> = ({
}}
/>
{/* ─── 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}
</Typography>
)}
{/* Features */}
{/* ─── Features ─── */}
{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
</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) => (
<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>
</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>
)}
</WizardLayout>

View File

@@ -257,24 +257,68 @@ const GridSidebarLayout: React.FC<{
</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<{
children: React.ReactNode;
secondaryPanel?: React.ReactNode;
}> = ({ children, secondaryPanel }) => (
<Container maxWidth="lg" sx={{ flex: 1, py: 3 }}>
backLink?: React.ReactNode;
}> = ({ children, secondaryPanel, backLink }) => (
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Left panel — scrollable content */}
<Box
sx={{
display: 'flex',
gap: { xs: 0, md: 4 },
flexDirection: { xs: 'column', md: 'row' },
mb: 4,
width: { xs: '100%', md: '55%' },
flexShrink: 0,
overflowY: 'auto',
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>
<Box sx={{ width: { xs: '100%', md: '50%' } }}>{secondaryPanel}</Box>
{backLink && <Box sx={{ pt: 1.5 }}>{backLink}</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>
</Container>
);
// ─── Variant map ─────────────────────────────────────────────────────────────
@@ -343,8 +387,8 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
flexDirection: 'column',
minHeight: '100vh',
bgcolor: 'background.default',
// list-map: lock to viewport so only the left panel scrolls
...(variant === 'list-map' && {
// list-map + detail-toggles: lock to viewport so panels scroll independently
...((variant === 'list-map' || variant === 'detail-toggles') && {
height: '100vh',
overflow: 'hidden',
}),
@@ -358,8 +402,8 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
{/* Stepper + running total bar (grid-sidebar, detail-toggles only) */}
{showStepper && <StepperBar stepper={progressStepper} total={runningTotal} />}
{/* Back link — inside left panel for list-map, above content for others */}
{showBackLink && variant !== 'list-map' && (
{/* Back link — inside left panel for list-map/detail-toggles, above content for others */}
{showBackLink && variant !== 'list-map' && variant !== 'detail-toggles' && (
<Container
maxWidth={variant === 'centered-form' ? 'sm' : 'lg'}
sx={{ pt: 2, px: { xs: 4, md: 3 } }}
@@ -376,7 +420,7 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
<LayoutComponent
secondaryPanel={secondaryPanel}
backLink={
showBackLink && variant === 'list-map' ? (
showBackLink && (variant === 'list-map' || variant === 'detail-toggles') ? (
<Box sx={{ pt: 1.5 }}>
<BackLink label={backLabel} onClick={onBack} />
</Box>