feat: add per-event hero logo customization options

Add configurable hero logo settings for individual events:
- Logo visibility toggle (show/hide in hero section)
- Logo size options (small, medium, large, xlarge)
- Logo position options (top, center, bottom)

Changes include:
- Database migration for hero_logo_visible, hero_logo_size, hero_logo_position fields
- Backend routes updated to handle new settings
- Frontend admin page with logo customization controls
- HeroGalleryLayout component with dynamic logo rendering
- i18n translations for EN and DE

Also updates .gitignore to exclude test files and artifacts.
This commit is contained in:
Paul Nothaft
2026-01-22 13:54:23 +01:00
parent f8881d5bd6
commit 0790a1ddad
11 changed files with 353 additions and 27 deletions
@@ -689,6 +689,9 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
disableRightClick={disableRightClick}
enableDevtoolsProtection={enableDevtoolsProtection}
useCanvasRendering={useCanvasRendering}
heroLogoVisible={data?.event?.hero_logo_visible !== false}
heroLogoSize={data?.event?.hero_logo_size || 'medium'}
heroLogoPosition={data?.event?.hero_logo_position || 'top'}
/>
</div>
@@ -52,6 +52,10 @@ interface PhotoGridWithLayoutsProps {
requireNameEmail?: boolean;
};
onFeedbackChange?: () => void;
// Hero logo customization options
heroLogoVisible?: boolean;
heroLogoSize?: 'small' | 'medium' | 'large' | 'xlarge';
heroLogoPosition?: 'top' | 'center' | 'bottom';
}
export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
@@ -76,7 +80,10 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
eventName,
eventLogo,
eventDate,
expiresAt
expiresAt,
heroLogoVisible = true,
heroLogoSize = 'medium',
heroLogoPosition = 'top'
}) => {
const { t } = useTranslation();
const { theme } = useTheme();
@@ -200,6 +207,9 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
expiresAt,
feedbackEnabled,
feedbackOptions,
heroLogoVisible,
heroLogoSize,
heroLogoPosition,
};
let LayoutComponent;
@@ -18,6 +18,10 @@ interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
expiresAt?: string;
// Use a static hero photo independent of current filter
heroPhotoOverride?: Photo | null;
// Hero logo customization options
heroLogoVisible?: boolean;
heroLogoSize?: 'small' | 'medium' | 'large' | 'xlarge';
heroLogoPosition?: 'top' | 'center' | 'bottom';
}
export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
@@ -34,6 +38,9 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
eventDate,
expiresAt,
heroPhotoOverride,
heroLogoVisible = true,
heroLogoSize = 'medium',
heroLogoPosition = 'top',
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
@@ -51,6 +58,22 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
const gallerySettings = theme.gallerySettings || {};
const overlayOpacity = gallerySettings.heroOverlayOpacity || 0.3;
// Helper function to get logo size classes
const getLogoSizeClasses = (size: string): string => {
switch (size) {
case 'small':
return 'h-12 sm:h-14 lg:h-16';
case 'medium':
return 'h-20 sm:h-24 lg:h-32';
case 'large':
return 'h-28 sm:h-32 lg:h-40';
case 'xlarge':
return 'h-36 sm:h-40 lg:h-48';
default:
return 'h-20 sm:h-24 lg:h-32';
}
};
const [likedIds, setLikedIds] = useState<Set<number>>(new Set());
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
const gridRef = useRef<HTMLDivElement | null>(null);
@@ -133,31 +156,51 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
{/* Hero Content */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center px-4">
{/* Logo - Show custom logo or fallback to PicPeak logo */}
<div className="mb-6">
<img
src={eventLogo ?
buildResourceUrl(eventLogo) :
'/picpeak-logo-transparent.png'
}
alt="Event logo"
className="h-20 sm:h-24 lg:h-32 mx-auto"
style={{
// Only apply brightness/invert filter to default logo; custom logos display as-is
filter: eventLogo
? 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
}}
/>
</div>
{/* Logo at top position */}
{heroLogoVisible && heroLogoPosition === 'top' && (
<div className="mb-6">
<img
src={eventLogo ?
buildResourceUrl(eventLogo) :
'/picpeak-logo-transparent.png'
}
alt="Event logo"
className={`${getLogoSizeClasses(heroLogoSize)} mx-auto`}
style={{
filter: eventLogo
? 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
}}
/>
</div>
)}
{/* Event Title */}
{eventName && (
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-white drop-shadow-lg mb-4">
{eventName}
</h1>
)}
{/* Logo at center position (between title and dates) */}
{heroLogoVisible && heroLogoPosition === 'center' && (
<div className="my-6">
<img
src={eventLogo ?
buildResourceUrl(eventLogo) :
'/picpeak-logo-transparent.png'
}
alt="Event logo"
className={`${getLogoSizeClasses(heroLogoSize)} mx-auto`}
style={{
filter: eventLogo
? 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
}}
/>
</div>
)}
{/* Event Dates */}
{(eventDate || expiresAt) && (
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 text-white/90">
@@ -175,6 +218,25 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
)}
</div>
)}
{/* Logo at bottom position */}
{heroLogoVisible && heroLogoPosition === 'bottom' && (
<div className="mt-6">
<img
src={eventLogo ?
buildResourceUrl(eventLogo) :
'/picpeak-logo-transparent.png'
}
alt="Event logo"
className={`${getLogoSizeClasses(heroLogoSize)} mx-auto`}
style={{
filter: eventLogo
? 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
}}
/>
</div>
)}
</div>
</div>