fix: render minimal/none header styles, cap hero height, switch category hero images (#158, #162, #163)
- Add distinct rendering branches for minimal and none header styles in GalleryLayout (grid and non-grid), skipping the colored banner/wave divider for both - Cap hero section height at 700px via max-h to prevent it dominating ultra-wide viewports - Watch selectedCategoryId in GalleryView and swap the hero photo to the category's hero_photo_id when filtering, reverting to the event default when cleared - Add minimal/none preview branches in GalleryPreview so the admin theme editor shows visually distinct previews for all four styles - Remove unused AdminPhoto import that was blocking the build - Add Playwright e2e tests covering all four header styles, hero max height, and category hero switching
This commit is contained in:
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Plus, X, Loader2, Image as ImageIcon, Check } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { categoriesService, type PhotoCategory } from '../../services/categories.service';
|
||||
import { photosService, type AdminPhoto } from '../../services/photos.service';
|
||||
import { photosService } from '../../services/photos.service';
|
||||
import { Button, Card, AuthenticatedImage } from '../common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@@ -92,8 +92,10 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
|
||||
? 'justify-end text-right flex-row-reverse'
|
||||
: 'justify-start text-left';
|
||||
|
||||
// Check if hero header style is selected
|
||||
// Check header style
|
||||
const isHeroHeader = theme.headerStyle === 'hero';
|
||||
const isMinimalHeader = theme.headerStyle === 'minimal';
|
||||
const isNoHeader = theme.headerStyle === 'none';
|
||||
const heroDividerStyle: HeroDividerStyle = theme.heroDividerStyle || 'wave';
|
||||
|
||||
// Render hero divider based on style
|
||||
@@ -210,7 +212,7 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
|
||||
fontFamily: theme.fontFamily || 'Inter, sans-serif',
|
||||
}}
|
||||
>
|
||||
{/* Hero Header - shown when headerStyle is 'hero' */}
|
||||
{/* Hero Header */}
|
||||
{isHeroHeader && (
|
||||
<div
|
||||
className="relative text-white overflow-hidden"
|
||||
@@ -221,7 +223,6 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
|
||||
>
|
||||
<div className="py-8 px-4 relative z-10">
|
||||
<div className="text-center max-w-md mx-auto">
|
||||
{/* Logo in Hero */}
|
||||
{showLogo && (
|
||||
<div className="mb-3">
|
||||
{resolvedLogoUrl ? (
|
||||
@@ -238,7 +239,6 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Event Name */}
|
||||
<h1
|
||||
className="text-xl font-bold mb-2"
|
||||
style={{
|
||||
@@ -248,22 +248,20 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
|
||||
>
|
||||
Sample Event
|
||||
</h1>
|
||||
{/* Event Date */}
|
||||
<div className="flex items-center justify-center text-white/80 text-sm" style={{ textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)' }}>
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
<span>January 15, 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Divider */}
|
||||
<div className="absolute bottom-0 left-0 right-0">
|
||||
{renderHeroDivider()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Header - shown when headerStyle is NOT 'hero' */}
|
||||
{!isHeroHeader && (
|
||||
{/* Standard Header */}
|
||||
{!isHeroHeader && !isMinimalHeader && !isNoHeader && (
|
||||
<div
|
||||
className="px-4 py-3 border-b space-y-2"
|
||||
style={{
|
||||
@@ -299,10 +297,32 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Minimal Header - thin bar with just event name */}
|
||||
{isMinimalHeader && (
|
||||
<div
|
||||
className="px-4 py-2 border-b"
|
||||
style={{
|
||||
borderColor: theme.primaryColor ? `${theme.primaryColor}20` : '#e5e7eb',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-sm font-semibold truncate"
|
||||
style={{
|
||||
fontFamily: theme.headingFontFamily || theme.fontFamily || 'Inter, sans-serif',
|
||||
}}
|
||||
>
|
||||
Sample Event
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* None Header - no header content at all */}
|
||||
{/* (isNoHeader renders nothing here — goes straight to layout bar) */}
|
||||
|
||||
{/* Layout info bar */}
|
||||
<div className="px-4 py-1 border-b text-xs text-neutral-500 flex justify-between" style={{ borderColor: theme.primaryColor ? `${theme.primaryColor}20` : '#e5e7eb' }}>
|
||||
<span>Gallery preview</span>
|
||||
<span className="capitalize">{isHeroHeader ? `Hero + ${activeLayout}` : `${activeLayout} layout`}</span>
|
||||
<span className="capitalize">{isHeroHeader ? `Hero + ${activeLayout}` : isMinimalHeader ? `Minimal + ${activeLayout}` : isNoHeader ? `No header + ${activeLayout}` : `${activeLayout} layout`}</span>
|
||||
</div>
|
||||
|
||||
{/* Preview Content */}
|
||||
|
||||
@@ -63,6 +63,8 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
// Determine header style - use prop first (from event data), then theme, then fall back to 'standard'
|
||||
const headerStyle: HeaderStyleType = headerStyleProp || theme.headerStyle || 'standard';
|
||||
const isHeroHeader = headerStyle === 'hero';
|
||||
const isMinimalHeader = headerStyle === 'minimal';
|
||||
const isNoHeader = headerStyle === 'none';
|
||||
|
||||
// Non-grid layouts that need the sidebar (excluding layouts using hero header)
|
||||
const isNonGridLayout = theme.galleryLayout && theme.galleryLayout !== 'grid';
|
||||
@@ -132,8 +134,8 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
|
||||
{/* Header structure */}
|
||||
<header className={`gallery-header bg-white border-b border-neutral-200 sticky top-0 z-40 ${isNonGridLayout || isHeroHeader ? 'shadow-sm' : ''}`}>
|
||||
{/* For non-grid layouts - keep the current structure */}
|
||||
{isNonGridLayout && !isHeroHeader && (
|
||||
{/* For non-grid layouts - keep the current structure (standard and minimal/none) */}
|
||||
{isNonGridLayout && !isHeroHeader && !isMinimalHeader && !isNoHeader && (
|
||||
<div className="bg-neutral-50 border-b border-neutral-200">
|
||||
<div className="container py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -179,7 +181,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
)}
|
||||
|
||||
{/* For grid layout - everything in one bar (standard header) */}
|
||||
{!isNonGridLayout && !isHeroHeader && (
|
||||
{!isNonGridLayout && !isHeroHeader && !isMinimalHeader && !isNoHeader && (
|
||||
<div className="container py-3">
|
||||
<div className="flex items-center justify-between gap-2 sm:gap-4">
|
||||
{/* Left side - Menu button, Logo */}
|
||||
@@ -300,6 +302,134 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* For minimal/none header + non-grid layouts - compact menu bar */}
|
||||
{isNonGridLayout && (isMinimalHeader || isNoHeader) && (
|
||||
<div className="bg-neutral-50 border-b border-neutral-200">
|
||||
<div className="container py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{menuButton}
|
||||
{headerExtra}
|
||||
{isMinimalHeader && (
|
||||
<h1
|
||||
className="text-sm font-semibold text-neutral-900 truncate"
|
||||
style={{ fontFamily: headingFontFamily }}
|
||||
>
|
||||
{event.event_name}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{showDownloadAll && onDownloadAll && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
leftIcon={<Download className="w-4 h-4" />}
|
||||
onClick={onDownloadAll}
|
||||
isLoading={isDownloading}
|
||||
className="gallery-btn gallery-btn-download"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
|
||||
<span className="sm:hidden">{t('common.download')}</span>
|
||||
</Button>
|
||||
)}
|
||||
{showLogout && onLogout && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<LogOut className="w-4 h-4" />}
|
||||
onClick={onLogout}
|
||||
className="gallery-btn gallery-btn-logout sm:min-w-0"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('common.logout')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* For minimal header + grid layout - compact bar with event name */}
|
||||
{!isNonGridLayout && isMinimalHeader && (
|
||||
<div className="container py-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{menuButton}
|
||||
<h1
|
||||
className="text-sm font-semibold text-neutral-900 truncate"
|
||||
style={{ fontFamily: headingFontFamily }}
|
||||
>
|
||||
{event.event_name}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{headerExtra}
|
||||
{showDownloadAll && onDownloadAll && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
leftIcon={<Download className="w-4 h-4" />}
|
||||
onClick={onDownloadAll}
|
||||
isLoading={isDownloading}
|
||||
className="gallery-btn gallery-btn-download hidden sm:flex"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
|
||||
</Button>
|
||||
)}
|
||||
{showLogout && onLogout && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<LogOut className="w-4 h-4" />}
|
||||
onClick={onLogout}
|
||||
className="gallery-btn gallery-btn-logout sm:min-w-0"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('common.logout')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* For none header + grid layout - just functional buttons, no event info */}
|
||||
{!isNonGridLayout && isNoHeader && (
|
||||
<div className="container py-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{menuButton}
|
||||
{headerExtra}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{showDownloadAll && onDownloadAll && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
leftIcon={<Download className="w-4 h-4" />}
|
||||
onClick={onDownloadAll}
|
||||
isLoading={isDownloading}
|
||||
className="gallery-btn gallery-btn-download hidden sm:flex"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
|
||||
</Button>
|
||||
)}
|
||||
{showLogout && onLogout && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<LogOut className="w-4 h-4" />}
|
||||
onClick={onLogout}
|
||||
className="gallery-btn gallery-btn-logout sm:min-w-0"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('common.logout')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* For hero header style - minimal header with just menu and logout */}
|
||||
{isHeroHeader && (
|
||||
<div className="container py-3">
|
||||
@@ -345,8 +475,8 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Hero Header for non-grid layouts when using standard header style */}
|
||||
{isNonGridLayout && !isHeroHeader && (
|
||||
{/* Colored banner for non-grid layouts when using standard header style */}
|
||||
{isNonGridLayout && !isHeroHeader && !isMinimalHeader && !isNoHeader && (
|
||||
<div
|
||||
className="gallery-hero relative text-white overflow-hidden"
|
||||
style={{
|
||||
|
||||
@@ -236,9 +236,11 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
}
|
||||
}, [showMediaFilter, mediaFilter]);
|
||||
|
||||
// Determine a stable hero photo from the initial (unfiltered) load
|
||||
// Determine the default hero photo from the initial (unfiltered) load
|
||||
const [defaultHeroPhoto, setDefaultHeroPhoto] = useState<Photo | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!staticHeroPhoto && data?.photos && filterType === 'all') {
|
||||
if (!defaultHeroPhoto && data?.photos && filterType === 'all') {
|
||||
let hero: Photo | null = null;
|
||||
const heroId = data?.event?.hero_photo_id || null;
|
||||
if (heroId) {
|
||||
@@ -249,10 +251,29 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
hero = firstPhoto || data.photos[0];
|
||||
}
|
||||
if (hero) {
|
||||
setDefaultHeroPhoto(hero);
|
||||
setStaticHeroPhoto(hero);
|
||||
}
|
||||
}
|
||||
}, [data?.photos, data?.event?.hero_photo_id, filterType, staticHeroPhoto]);
|
||||
}, [data?.photos, data?.event?.hero_photo_id, filterType, defaultHeroPhoto]);
|
||||
|
||||
// Switch hero photo when a category with its own hero image is selected
|
||||
useEffect(() => {
|
||||
if (!data?.photos || !defaultHeroPhoto) return;
|
||||
|
||||
if (selectedCategoryId) {
|
||||
const category = (data.categories || []).find(c => c.id === selectedCategoryId);
|
||||
if (category?.hero_photo_id) {
|
||||
const categoryHero = data.photos.find(p => p.id === category.hero_photo_id);
|
||||
if (categoryHero) {
|
||||
setStaticHeroPhoto(categoryHero);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// No category selected or category has no hero — revert to default
|
||||
setStaticHeroPhoto(defaultHeroPhoto);
|
||||
}, [selectedCategoryId, data?.categories, data?.photos, defaultHeroPhoto]);
|
||||
|
||||
// Apply theme when settings are loaded
|
||||
useEffect(() => {
|
||||
|
||||
@@ -135,7 +135,7 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
|
||||
return (
|
||||
<div className="relative -mt-6">
|
||||
{/* Hero Section */}
|
||||
<div className="relative h-[60vh] sm:h-[70vh] lg:h-[80vh] -mx-4 sm:-mx-6 lg:-mx-8 mb-8">
|
||||
<div className="relative h-[60vh] sm:h-[70vh] lg:h-[80vh] max-h-[700px] -mx-4 sm:-mx-6 lg:-mx-8 mb-8">
|
||||
<AuthenticatedImage
|
||||
src={heroPhoto.url}
|
||||
fallbackSrc={heroPhoto.thumbnail_url || undefined}
|
||||
|
||||
Reference in New Issue
Block a user