fix: hero header state and preview in admin theme editor (#158)

- Add hero header rendering to GalleryPreview component with divider styles
- Support event-specific header_style prop in GalleryLayout
- Pass header_style from event data to GalleryLayout in GalleryView
- Divider options now properly show/hide when switching header styles

This ensures the live preview accurately reflects hero header changes
and event-specific header styles are respected in the gallery view.
This commit is contained in:
Paul Nothaft
2026-02-02 23:09:36 +01:00
parent fa4c83812d
commit f554f463b3
3 changed files with 131 additions and 39 deletions
+126 -37
View File
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { Camera } from 'lucide-react';
import { ThemeConfig, GalleryLayoutType } from '../../types/theme.types';
import { Camera, Calendar } from 'lucide-react';
import { ThemeConfig, GalleryLayoutType, HeroDividerStyle } from '../../types/theme.types';
import { buildResourceUrl } from '../../utils/url';
interface GalleryPreviewBranding {
@@ -92,6 +92,39 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
? 'justify-end text-right flex-row-reverse'
: 'justify-start text-left';
// Check if hero header style is selected
const isHeroHeader = theme.headerStyle === 'hero';
const heroDividerStyle: HeroDividerStyle = theme.heroDividerStyle || 'wave';
// Render hero divider based on style
const renderHeroDivider = () => {
const bgColor = theme.backgroundColor || '#fafafa';
switch (heroDividerStyle) {
case 'wave':
return (
<svg className="w-full h-6" viewBox="0 0 1200 120" preserveAspectRatio="none">
<path d="M0,60 C150,90 350,30 600,60 C850,90 1050,30 1200,60 L1200,120 L0,120 Z" fill={bgColor} />
</svg>
);
case 'curve':
return (
<svg className="w-full h-6" viewBox="0 0 1200 120" preserveAspectRatio="none">
<path d="M0,120 Q600,0 1200,120 L1200,120 L0,120 Z" fill={bgColor} />
</svg>
);
case 'angle':
return (
<svg className="w-full h-6" viewBox="0 0 1200 120" preserveAspectRatio="none">
<path d="M0,120 L600,40 L1200,120 L1200,120 L0,120 Z" fill={bgColor} />
</svg>
);
case 'straight':
case 'none':
default:
return null;
}
};
const renderLayout = () => {
const spacing = theme.gallerySettings?.spacing || 'normal';
const gapClass = spacing === 'tight' ? 'gap-1' : spacing === 'relaxed' ? 'gap-4' : 'gap-2';
@@ -169,7 +202,7 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
};
return (
<div
<div
className={`bg-white rounded-lg shadow-sm overflow-hidden ${className}`}
style={{
backgroundColor: theme.backgroundColor || '#ffffff',
@@ -177,45 +210,101 @@ export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
fontFamily: theme.fontFamily || 'Inter, sans-serif',
}}
>
{/* Preview Header */}
<div
className="px-4 py-3 border-b space-y-2"
style={{
borderColor: theme.primaryColor ? `${theme.primaryColor}20` : '#e5e7eb',
}}
>
<div className={`flex items-center gap-3 ${brandFlexClass}`}>
{showLogo && (
resolvedLogoUrl ? (
<img
src={resolvedLogoUrl}
alt={brandName}
className="h-8 w-auto object-contain"
/>
) : (
<div className="h-8 w-8 rounded-full bg-neutral-200 flex items-center justify-center">
<Camera className="w-4 h-4 text-neutral-500" />
</div>
)
)}
{showText && (
<div>
<p className="text-sm font-semibold leading-tight">{brandName}</p>
{brandTagline && (
<p className="text-xs text-neutral-500 leading-tight">{brandTagline}</p>
{/* Hero Header - shown when headerStyle is 'hero' */}
{isHeroHeader && (
<div
className="relative text-white overflow-hidden"
style={{
backgroundColor: theme.accentColor || theme.primaryColor || '#22c55e',
backgroundImage: 'url("data:image/svg+xml,%3Csvg width=\'40\' height=\'40\' viewBox=\'0 0 40 40\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cg fill=\'%23ffffff\' fill-opacity=\'0.03\'%3E%3Cpath d=\'M0 40L40 0H20L0 20M40 40V20L20 40\'/%3E%3C/g%3E%3C/svg%3E")',
}}
>
<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 ? (
<img
src={resolvedLogoUrl}
alt={brandName}
className="h-10 w-auto object-contain mx-auto"
style={{ filter: 'brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))' }}
/>
) : (
<div className="h-10 w-10 rounded-full bg-white/20 flex items-center justify-center mx-auto">
<Camera className="w-5 h-5 text-white" />
</div>
)}
</div>
)}
{/* Event Name */}
<h1
className="text-xl font-bold mb-2"
style={{
fontFamily: theme.headingFontFamily || theme.fontFamily || 'Inter, sans-serif',
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)'
}}
>
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>
)}
{!showLogo && !showText && (
<p className="text-sm font-semibold">{brandName}</p>
)}
</div>
{/* Divider */}
<div className="absolute bottom-0 left-0 right-0">
{renderHeroDivider()}
</div>
</div>
<div className="text-xs text-neutral-500 flex justify-between">
<span>Gallery preview</span>
<span className="capitalize">{activeLayout} layout</span>
)}
{/* Standard Header - shown when headerStyle is NOT 'hero' */}
{!isHeroHeader && (
<div
className="px-4 py-3 border-b space-y-2"
style={{
borderColor: theme.primaryColor ? `${theme.primaryColor}20` : '#e5e7eb',
}}
>
<div className={`flex items-center gap-3 ${brandFlexClass}`}>
{showLogo && (
resolvedLogoUrl ? (
<img
src={resolvedLogoUrl}
alt={brandName}
className="h-8 w-auto object-contain"
/>
) : (
<div className="h-8 w-8 rounded-full bg-neutral-200 flex items-center justify-center">
<Camera className="w-4 h-4 text-neutral-500" />
</div>
)
)}
{showText && (
<div>
<p className="text-sm font-semibold leading-tight">{brandName}</p>
{brandTagline && (
<p className="text-xs text-neutral-500 leading-tight">{brandTagline}</p>
)}
</div>
)}
{!showLogo && !showText && (
<p className="text-sm font-semibold">{brandName}</p>
)}
</div>
</div>
)}
{/* 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>
</div>
{/* Preview Content */}
<div className="p-4" style={{ maxHeight: '400px', overflowY: 'auto' }}>
{renderLayout()}
@@ -39,6 +39,7 @@ interface GalleryLayoutProps {
isDownloading?: boolean;
headerExtra?: React.ReactNode;
menuButton?: React.ReactNode;
headerStyle?: HeaderStyleType;
children: React.ReactNode;
}
@@ -52,14 +53,15 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
isDownloading = false,
headerExtra,
menuButton,
headerStyle: headerStyleProp,
children,
}) => {
const { t } = useTranslation();
const { format } = useLocalizedDate();
const { theme } = useTheme();
// Determine header style - check theme.headerStyle first, then fall back to legacy behavior
const headerStyle: HeaderStyleType = theme.headerStyle || 'standard';
// 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';
// Non-grid layouts that need the sidebar (excluding layouts using hero header)
@@ -584,6 +584,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
<GalleryLayout
event={event}
brandingSettings={brandingSettings}
headerStyle={data?.event?.header_style || theme.headerStyle}
showLogout={true}
onLogout={logout}
showDownloadAll={!showSidebar && allowDownloads}