diff --git a/src/components/molecules/ImageGallery/ImageGallery.stories.tsx b/src/components/molecules/ImageGallery/ImageGallery.stories.tsx new file mode 100644 index 0000000..8d497bf --- /dev/null +++ b/src/components/molecules/ImageGallery/ImageGallery.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ImageGallery } from './ImageGallery'; +import type { GalleryImage } from './ImageGallery'; +import Box from '@mui/material/Box'; + +const venueImages: GalleryImage[] = [ + { + src: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&h=600&fit=crop', + alt: 'Chapel interior with natural light', + }, + { + src: 'https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?w=800&h=600&fit=crop', + alt: 'Chapel exterior and gardens', + }, + { + src: 'https://images.unsplash.com/photo-1519167758481-83f550bb49b3?w=800&h=600&fit=crop', + alt: 'Reception hall', + }, + { + src: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&h=600&fit=crop', + alt: 'Lakeside view', + }, + { + src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop', + alt: 'Mountain chapel', + }, +]; + +const meta: Meta = { + title: 'Molecules/ImageGallery', + component: ImageGallery, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Default — multiple images with thumbnail strip */ +export const Default: Story = { + args: { + images: venueImages, + }, +}; + +/** Single image — no thumbnail strip shown */ +export const SingleImage: Story = { + args: { + images: [venueImages[0]], + }, +}; + +/** Two images */ +export const TwoImages: Story = { + args: { + images: venueImages.slice(0, 2), + }, +}; + +/** Custom hero height and thumbnail size */ +export const CustomSizes: Story = { + args: { + images: venueImages, + heroHeight: 300, + thumbnailSize: 56, + }, +}; diff --git a/src/components/molecules/ImageGallery/ImageGallery.tsx b/src/components/molecules/ImageGallery/ImageGallery.tsx new file mode 100644 index 0000000..7ac1db8 --- /dev/null +++ b/src/components/molecules/ImageGallery/ImageGallery.tsx @@ -0,0 +1,163 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import type { SxProps, Theme } from '@mui/material/styles'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** A single image in the gallery */ +export interface GalleryImage { + /** Image URL */ + src: string; + /** Alt text for accessibility */ + alt: string; +} + +/** Props for the FA ImageGallery component */ +export interface ImageGalleryProps { + /** Array of images to display */ + images: GalleryImage[]; + /** Height of the hero image area */ + heroHeight?: number | { xs?: number; sm?: number; md?: number; lg?: number }; + /** Height of each thumbnail */ + thumbnailSize?: number; + /** MUI sx prop for style overrides */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Image gallery with hero display and thumbnail strip. + * + * Shows a large hero image with a row of clickable thumbnails below. + * Hovering a thumbnail previews it in the hero; clicking locks the + * selection. First image is selected by default. + * + * Used on venue detail, coffin detail, and other product pages. + * + * Usage: + * ```tsx + * + * ``` + */ +export const ImageGallery = React.forwardRef( + ({ images, heroHeight = { xs: 280, md: 420 }, thumbnailSize = 72, sx }, ref) => { + const [selectedIndex, setSelectedIndex] = React.useState(0); + const [hoverIndex, setHoverIndex] = React.useState(null); + + // The image shown in the hero: hovered thumbnail takes priority over selected + const displayIndex = hoverIndex ?? selectedIndex; + const displayImage = images[displayIndex] ?? images[0]; + + if (!images.length) return null; + + // Single image — no thumbnails needed + if (images.length === 1) { + return ( + + + + ); + } + + return ( + + {/* Hero image */} + + + {/* Thumbnail strip */} + + {images.map((image, index) => ( + setSelectedIndex(index)} + onMouseEnter={() => setHoverIndex(index)} + onMouseLeave={() => setHoverIndex(null)} + sx={{ + width: thumbnailSize, + height: thumbnailSize, + flexShrink: 0, + borderRadius: 1, + backgroundImage: `url(${image.src})`, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundColor: 'var(--fa-color-surface-subtle)', + cursor: 'pointer', + border: '2px solid', + borderColor: index === selectedIndex ? 'primary.main' : 'transparent', + opacity: index === selectedIndex ? 1 : 0.7, + transition: 'border-color 150ms ease-in-out, opacity 150ms ease-in-out', + '&:hover': { + opacity: 1, + borderColor: + index === selectedIndex ? 'primary.main' : 'var(--fa-color-border-default)', + }, + }} + aria-label={image.alt} + aria-current={index === selectedIndex ? 'true' : undefined} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setSelectedIndex(index); + } + }} + /> + ))} + + + ); + }, +); + +ImageGallery.displayName = 'ImageGallery'; +export default ImageGallery; diff --git a/src/components/molecules/ImageGallery/index.ts b/src/components/molecules/ImageGallery/index.ts new file mode 100644 index 0000000..4b4b355 --- /dev/null +++ b/src/components/molecules/ImageGallery/index.ts @@ -0,0 +1,2 @@ +export { ImageGallery, default } from './ImageGallery'; +export type { ImageGalleryProps, GalleryImage } from './ImageGallery'; diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx index 56e6e51..ef64c32 100644 --- a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx @@ -2,6 +2,8 @@ import React from 'react'; import Box from '@mui/material/Box'; import type { SxProps, Theme } from '@mui/material/styles'; import { WizardLayout } from '../../templates/WizardLayout'; +import { ImageGallery } from '../../molecules/ImageGallery'; +import type { GalleryImage } from '../../molecules/ImageGallery'; import { Typography } from '../../atoms/Typography'; import { Button } from '../../atoms/Button'; @@ -17,6 +19,8 @@ export interface CoffinSpec { export interface CoffinProfile { name: string; imageUrl: string; + /** Additional gallery images (if provided, imageUrl becomes the first) */ + images?: GalleryImage[]; description?: string; specs?: CoffinSpec[]; price: number; @@ -163,20 +167,15 @@ export const CoffinDetailsStep: React.FC = ({ } > - {/* Left panel: image + description */} - 0 + ? coffin.images + : [{ src: coffin.imageUrl, alt: `Photo of ${coffin.name}` }] + } + heroHeight={{ xs: 280, md: 400 }} + sx={{ mb: 3 }} /> {coffin.description && ( diff --git a/src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx b/src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx index 82b46ee..d432c47 100644 --- a/src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx +++ b/src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx @@ -37,6 +37,24 @@ const sampleVenue: VenueDetail = { id: 'west-chapel', name: 'West Chapel', imageUrl: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&h=600&fit=crop', + images: [ + { + src: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&h=600&fit=crop', + alt: 'Chapel interior', + }, + { + src: 'https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?w=800&h=600&fit=crop', + alt: 'Garden area', + }, + { + src: 'https://images.unsplash.com/photo-1519167758481-83f550bb49b3?w=800&h=600&fit=crop', + alt: 'Reception hall', + }, + { + src: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&h=600&fit=crop', + alt: 'Lakeside pavilion', + }, + ], location: 'Strathfield', venueType: 'Indoor Chapel', capacity: 120, diff --git a/src/components/pages/VenueDetailStep/VenueDetailStep.tsx b/src/components/pages/VenueDetailStep/VenueDetailStep.tsx index 496d362..1577db3 100644 --- a/src/components/pages/VenueDetailStep/VenueDetailStep.tsx +++ b/src/components/pages/VenueDetailStep/VenueDetailStep.tsx @@ -6,6 +6,8 @@ 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 { ImageGallery } from '../../molecules/ImageGallery'; +import type { GalleryImage } from '../../molecules/ImageGallery'; import { Card } from '../../atoms/Card'; import { Chip } from '../../atoms/Chip'; import { Typography } from '../../atoms/Typography'; @@ -19,6 +21,8 @@ export interface VenueDetail { id: string; name: string; imageUrl: string; + /** Additional gallery images (if provided, imageUrl becomes the first) */ + images?: GalleryImage[]; location: string; venueType?: string; capacity?: number; @@ -221,20 +225,15 @@ export const VenueDetailStep: React.FC = ({ > {/* ═══════ LEFT PANEL: scrollable content ═══════ */} - {/* ─── Hero image ─── */} - 0 + ? venue.images + : [{ src: venue.imageUrl, alt: `Photo of ${venue.name}` }] + } + heroHeight={{ xs: 280, md: 420 }} + sx={{ mb: 3 }} /> {/* ─── Description ─── */}