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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user