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:
@@ -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>
|
||||
|
||||
|
||||
@@ -833,6 +833,22 @@
|
||||
"protectionLevelStandard": "Standard - Tastenkombinationen blockiert",
|
||||
"protectionLevelEnhanced": "Erweitert - Bildschirmfoto-Erkennung",
|
||||
"protectionLevelMaximum": "Maximum - DevTools-Erkennung & Canvas-Rendering",
|
||||
"heroLogoSettings": "Hero-Logo-Einstellungen",
|
||||
"heroLogoVisible": "Logo im Hero-Bereich anzeigen",
|
||||
"heroLogoSize": "Logo-Größe",
|
||||
"heroLogoSizeSmall": "Klein",
|
||||
"heroLogoSizeMedium": "Mittel",
|
||||
"heroLogoSizeLarge": "Groß",
|
||||
"heroLogoSizeXLarge": "Extra Groß",
|
||||
"heroLogoPosition": "Logo-Position",
|
||||
"heroLogoPositionTop": "Oben (über dem Titel)",
|
||||
"heroLogoPositionCenter": "Mitte (zwischen Titel und Datum)",
|
||||
"heroLogoPositionBottom": "Unten (unter dem Datum)",
|
||||
"heroLogoInfo": "Diese Einstellungen gelten für das Hero-Layout der Galerie. Sie können das Logo ausblenden oder Größe und Position anpassen.",
|
||||
"heroLogoVisibleLabel": "Logo sichtbar",
|
||||
"heroLogoSizeLabel": "Größe",
|
||||
"heroLogoPositionLabel": "Position",
|
||||
"heroLogoHidden": "Logo ausgeblendet",
|
||||
"heroPhoto": "Hero-Foto",
|
||||
"heroPhotoHelp": "Wählen Sie ein hervorgehobenes Foto für das Hero-Galerie-Layout",
|
||||
"selectHeroPhoto": "Hero-Foto auswählen",
|
||||
|
||||
@@ -459,6 +459,22 @@
|
||||
"protectionLevelStandard": "Standard - Keyboard shortcuts blocked",
|
||||
"protectionLevelEnhanced": "Enhanced - Print screen detection",
|
||||
"protectionLevelMaximum": "Maximum - DevTools detection & canvas rendering",
|
||||
"heroLogoSettings": "Hero Logo Settings",
|
||||
"heroLogoVisible": "Display logo in hero section",
|
||||
"heroLogoSize": "Logo Size",
|
||||
"heroLogoSizeSmall": "Small",
|
||||
"heroLogoSizeMedium": "Medium",
|
||||
"heroLogoSizeLarge": "Large",
|
||||
"heroLogoSizeXLarge": "Extra Large",
|
||||
"heroLogoPosition": "Logo Position",
|
||||
"heroLogoPositionTop": "Top (above title)",
|
||||
"heroLogoPositionCenter": "Center (between title and dates)",
|
||||
"heroLogoPositionBottom": "Bottom (below dates)",
|
||||
"heroLogoInfo": "These settings apply when the gallery uses the Hero layout. You can hide the logo or customize its size and position.",
|
||||
"heroLogoVisibleLabel": "Logo visible",
|
||||
"heroLogoSizeLabel": "Size",
|
||||
"heroLogoPositionLabel": "Position",
|
||||
"heroLogoHidden": "Logo hidden",
|
||||
"heroPhoto": "Hero Photo",
|
||||
"heroPhotoHelp": "Select a featured photo for the hero gallery layout",
|
||||
"selectHeroPhoto": "Select Hero Photo",
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
Shield,
|
||||
Monitor,
|
||||
Droplets,
|
||||
MousePointer
|
||||
MousePointer,
|
||||
Layout
|
||||
} from 'lucide-react';
|
||||
import { parseISO, differenceInDays, isValid } from 'date-fns';
|
||||
|
||||
@@ -160,6 +161,10 @@ export const EventDetailsPage: React.FC = () => {
|
||||
watermark_downloads: boolean;
|
||||
enable_devtools_protection: boolean;
|
||||
use_canvas_rendering: boolean;
|
||||
// Hero logo settings
|
||||
hero_logo_visible: boolean;
|
||||
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
hero_logo_position: 'top' | 'center' | 'bottom';
|
||||
};
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -184,6 +189,10 @@ export const EventDetailsPage: React.FC = () => {
|
||||
watermark_downloads: false,
|
||||
enable_devtools_protection: true,
|
||||
use_canvas_rendering: false,
|
||||
// Hero logo settings
|
||||
hero_logo_visible: true,
|
||||
hero_logo_size: 'medium',
|
||||
hero_logo_position: 'top',
|
||||
});
|
||||
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
|
||||
feedback_enabled: false,
|
||||
@@ -406,6 +415,10 @@ export const EventDetailsPage: React.FC = () => {
|
||||
watermark_downloads: event.watermark_downloads ?? false,
|
||||
enable_devtools_protection: event.enable_devtools_protection ?? true,
|
||||
use_canvas_rendering: event.use_canvas_rendering ?? false,
|
||||
// Load hero logo settings from event
|
||||
hero_logo_visible: event.hero_logo_visible ?? true,
|
||||
hero_logo_size: event.hero_logo_size || 'medium',
|
||||
hero_logo_position: event.hero_logo_position || 'top',
|
||||
});
|
||||
|
||||
setShowNewPassword(false);
|
||||
@@ -496,6 +509,10 @@ export const EventDetailsPage: React.FC = () => {
|
||||
watermark_downloads: editForm.watermark_downloads,
|
||||
enable_devtools_protection: editForm.enable_devtools_protection,
|
||||
use_canvas_rendering: editForm.use_canvas_rendering,
|
||||
// Hero logo settings
|
||||
hero_logo_visible: editForm.hero_logo_visible,
|
||||
hero_logo_size: editForm.hero_logo_size,
|
||||
hero_logo_position: editForm.hero_logo_position,
|
||||
};
|
||||
|
||||
// Only include fields that have defined values
|
||||
@@ -1062,6 +1079,66 @@ export const EventDetailsPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Logo Settings */}
|
||||
<div className="mt-4 pt-4 border-t border-neutral-200">
|
||||
<h3 className="text-sm font-semibold text-neutral-900 mb-3 flex items-center gap-2">
|
||||
<Layout className="w-4 h-4 text-primary-600" />
|
||||
{t('events.heroLogoSettings', 'Hero Logo Settings')}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.hero_logo_visible}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_visible: e.target.checked }))}
|
||||
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
|
||||
/>
|
||||
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
|
||||
<span className="text-sm text-neutral-700">{t('events.heroLogoVisible', 'Display logo in hero section')}</span>
|
||||
</label>
|
||||
|
||||
{editForm.hero_logo_visible && (
|
||||
<>
|
||||
<div className="ml-6">
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1">
|
||||
{t('events.heroLogoSize', 'Logo Size')}
|
||||
</label>
|
||||
<select
|
||||
value={editForm.hero_logo_size}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_size: e.target.value as 'small' | 'medium' | 'large' | 'xlarge' }))}
|
||||
className="w-full sm:w-48 px-3 py-2 border border-neutral-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-sm"
|
||||
>
|
||||
<option value="small">{t('events.heroLogoSizeSmall', 'Small')}</option>
|
||||
<option value="medium">{t('events.heroLogoSizeMedium', 'Medium')}</option>
|
||||
<option value="large">{t('events.heroLogoSizeLarge', 'Large')}</option>
|
||||
<option value="xlarge">{t('events.heroLogoSizeXLarge', 'Extra Large')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="ml-6">
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1">
|
||||
{t('events.heroLogoPosition', 'Logo Position')}
|
||||
</label>
|
||||
<select
|
||||
value={editForm.hero_logo_position}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_position: e.target.value as 'top' | 'center' | 'bottom' }))}
|
||||
className="w-full sm:w-48 px-3 py-2 border border-neutral-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-sm"
|
||||
>
|
||||
<option value="top">{t('events.heroLogoPositionTop', 'Top (above title)')}</option>
|
||||
<option value="center">{t('events.heroLogoPositionCenter', 'Center (between title and dates)')}</option>
|
||||
<option value="bottom">{t('events.heroLogoPositionBottom', 'Bottom (below dates)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-neutral-500 mt-2">
|
||||
{t('events.heroLogoInfo', 'These settings apply when the gallery uses the Hero layout. You can hide the logo or customize its size and position.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<dl className="space-y-4">
|
||||
@@ -1197,6 +1274,37 @@ export const EventDetailsPage: React.FC = () => {
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
{/* Hero Logo Settings Display */}
|
||||
<div className="pt-3 mt-3 border-t border-neutral-200">
|
||||
<dt className="text-sm font-medium text-neutral-500 flex items-center gap-2">
|
||||
<Layout className="w-4 h-4" />
|
||||
{t('events.heroLogoSettings', 'Hero Logo Settings')}
|
||||
</dt>
|
||||
<dd className="mt-2 text-sm text-neutral-900">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{event.hero_logo_visible !== false ? (
|
||||
<>
|
||||
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded">
|
||||
<Image className="w-3 h-3 mr-1" />
|
||||
{t('events.heroLogoVisibleLabel', 'Logo visible')}
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
|
||||
{t('events.heroLogoSizeLabel', 'Size')}: {event.hero_logo_size || 'medium'}
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
|
||||
{t('events.heroLogoPositionLabel', 'Position')}: {event.hero_logo_position || 'top'}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
|
||||
<Image className="w-3 h-3 mr-1" />
|
||||
{t('events.heroLogoHidden', 'Logo hidden')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -41,6 +41,10 @@ export interface Event {
|
||||
watermark_downloads?: boolean;
|
||||
enable_devtools_protection?: boolean;
|
||||
use_canvas_rendering?: boolean;
|
||||
// Hero logo customization fields
|
||||
hero_logo_visible?: boolean;
|
||||
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
hero_logo_position?: 'top' | 'center' | 'bottom';
|
||||
}
|
||||
|
||||
export interface GalleryInfo {
|
||||
|
||||
Reference in New Issue
Block a user