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:
@@ -94,3 +94,11 @@ backup/
|
||||
|
||||
# Local SQLite files in backend
|
||||
backend/*.sqlite*
|
||||
|
||||
# Test files and artifacts
|
||||
test-logo*.jpg
|
||||
test-logo*.png
|
||||
test-results/
|
||||
|
||||
# Development docker compose
|
||||
docker-compose.dev.yml
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Migration: Add hero logo customization settings to events table
|
||||
*
|
||||
* Allows per-event customization of the hero gallery logo:
|
||||
* - hero_logo_visible: Show/hide the logo overlay
|
||||
* - hero_logo_size: Logo size (small, medium, large, xlarge)
|
||||
* - hero_logo_position: Logo position (top, center, bottom)
|
||||
*
|
||||
* Addresses GitHub Issue #138: Add Option to customize the Hero gallery layout
|
||||
*/
|
||||
|
||||
exports.up = async function (knex) {
|
||||
console.log('Adding hero logo settings to events table...');
|
||||
|
||||
// Add hero_logo_visible column
|
||||
const hasVisibleColumn = await knex.schema.hasColumn('events', 'hero_logo_visible');
|
||||
if (!hasVisibleColumn) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.boolean('hero_logo_visible').notNullable().defaultTo(true);
|
||||
});
|
||||
console.log('Added hero_logo_visible column');
|
||||
}
|
||||
|
||||
// Add hero_logo_size column
|
||||
const hasSizeColumn = await knex.schema.hasColumn('events', 'hero_logo_size');
|
||||
if (!hasSizeColumn) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.string('hero_logo_size', 20).notNullable().defaultTo('medium');
|
||||
});
|
||||
console.log('Added hero_logo_size column');
|
||||
}
|
||||
|
||||
// Add hero_logo_position column
|
||||
const hasPositionColumn = await knex.schema.hasColumn('events', 'hero_logo_position');
|
||||
if (!hasPositionColumn) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.string('hero_logo_position', 20).notNullable().defaultTo('top');
|
||||
});
|
||||
console.log('Added hero_logo_position column');
|
||||
}
|
||||
|
||||
console.log('Migration 062_add_hero_logo_settings completed successfully');
|
||||
};
|
||||
|
||||
exports.down = async function (knex) {
|
||||
console.log('Rolling back hero logo settings...');
|
||||
|
||||
const hasVisibleColumn = await knex.schema.hasColumn('events', 'hero_logo_visible');
|
||||
if (hasVisibleColumn) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.dropColumn('hero_logo_visible');
|
||||
});
|
||||
}
|
||||
|
||||
const hasSizeColumn = await knex.schema.hasColumn('events', 'hero_logo_size');
|
||||
if (hasSizeColumn) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.dropColumn('hero_logo_size');
|
||||
});
|
||||
}
|
||||
|
||||
const hasPositionColumn = await knex.schema.hasColumn('events', 'hero_logo_position');
|
||||
if (hasPositionColumn) {
|
||||
await knex.schema.table('events', (table) => {
|
||||
table.dropColumn('hero_logo_position');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Hero logo settings columns dropped');
|
||||
};
|
||||
@@ -158,7 +158,11 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
body('disable_right_click').optional().isBoolean(),
|
||||
body('watermark_downloads').optional().isBoolean(),
|
||||
body('watermark_text').optional().trim(),
|
||||
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt()
|
||||
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
||||
// Hero logo settings
|
||||
body('hero_logo_visible').optional().isBoolean(),
|
||||
body('hero_logo_size').optional().isIn(['small', 'medium', 'large', 'xlarge']),
|
||||
body('hero_logo_position').optional().isIn(['top', 'center', 'bottom'])
|
||||
], async (req, res) => {
|
||||
try {
|
||||
logger.debug('Create event request body', { body: req.body });
|
||||
@@ -197,7 +201,11 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
moderate_comments = true,
|
||||
show_feedback_to_guests = true,
|
||||
// CSS Template
|
||||
css_template_id = null
|
||||
css_template_id = null,
|
||||
// Hero logo settings
|
||||
hero_logo_visible = true,
|
||||
hero_logo_size = 'medium',
|
||||
hero_logo_position = 'top'
|
||||
} = req.body;
|
||||
|
||||
const customerName = getCustomerNameFromPayload(req.body);
|
||||
@@ -335,7 +343,10 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
watermark_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false),
|
||||
watermark_text,
|
||||
require_password: formatBoolean(requirePassword),
|
||||
css_template_id: css_template_id || null
|
||||
css_template_id: css_template_id || null,
|
||||
hero_logo_visible: formatBoolean(hero_logo_visible !== undefined ? hero_logo_visible : true),
|
||||
hero_logo_size: hero_logo_size || 'medium',
|
||||
hero_logo_position: hero_logo_position || 'top'
|
||||
}).returning('id');
|
||||
|
||||
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
|
||||
@@ -612,7 +623,11 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt()
|
||||
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
||||
// Hero logo settings
|
||||
body('hero_logo_visible').optional().isBoolean(),
|
||||
body('hero_logo_size').optional().isIn(['small', 'medium', 'large', 'xlarge']),
|
||||
body('hero_logo_position').optional().isIn(['top', 'center', 'bottom'])
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
@@ -724,6 +739,11 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [
|
||||
updates.password_hash = await bcrypt.hash(crypto.randomBytes(32).toString('hex'), getBcryptRounds());
|
||||
}
|
||||
|
||||
// Format hero logo settings if provided
|
||||
if (Object.prototype.hasOwnProperty.call(updates, 'hero_logo_visible')) {
|
||||
updates.hero_logo_visible = formatBoolean(updates.hero_logo_visible);
|
||||
}
|
||||
|
||||
// Update event
|
||||
await db('events')
|
||||
.where('id', id)
|
||||
|
||||
@@ -115,7 +115,10 @@ router.get('/:slug/info', async (req, res) => {
|
||||
'require_password',
|
||||
'color_theme',
|
||||
'enable_devtools_protection',
|
||||
'use_canvas_rendering'
|
||||
'use_canvas_rendering',
|
||||
'hero_logo_visible',
|
||||
'hero_logo_size',
|
||||
'hero_logo_position'
|
||||
)
|
||||
.first();
|
||||
|
||||
@@ -162,7 +165,10 @@ router.get('/:slug/info', async (req, res) => {
|
||||
watermark_downloads: event.watermark_downloads === true || event.watermark_downloads === 1 || event.watermark_downloads === '1',
|
||||
watermark_text: event.watermark_text,
|
||||
enable_devtools_protection: event.enable_devtools_protection === true || event.enable_devtools_protection === 1 || event.enable_devtools_protection === '1',
|
||||
use_canvas_rendering: event.use_canvas_rendering === true || event.use_canvas_rendering === 1 || event.use_canvas_rendering === '1'
|
||||
use_canvas_rendering: event.use_canvas_rendering === true || event.use_canvas_rendering === 1 || event.use_canvas_rendering === '1',
|
||||
hero_logo_visible: event.hero_logo_visible !== false && event.hero_logo_visible !== 0 && event.hero_logo_visible !== '0',
|
||||
hero_logo_size: event.hero_logo_size || 'medium',
|
||||
hero_logo_position: event.hero_logo_position || 'top'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching gallery info:', error);
|
||||
@@ -332,6 +338,9 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||
watermark_text: req.event.watermark_text,
|
||||
enable_devtools_protection: req.event.enable_devtools_protection === true,
|
||||
use_canvas_rendering: req.event.use_canvas_rendering === true,
|
||||
hero_logo_visible: req.event.hero_logo_visible !== false && req.event.hero_logo_visible !== 0 && req.event.hero_logo_visible !== '0',
|
||||
hero_logo_size: req.event.hero_logo_size || 'medium',
|
||||
hero_logo_position: req.event.hero_logo_position || 'top',
|
||||
...protectionSettings
|
||||
},
|
||||
categories: categories,
|
||||
|
||||
@@ -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,7 +156,8 @@ 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 */}
|
||||
{/* Logo at top position */}
|
||||
{heroLogoVisible && heroLogoPosition === 'top' && (
|
||||
<div className="mb-6">
|
||||
<img
|
||||
src={eventLogo ?
|
||||
@@ -141,15 +165,15 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt="Event logo"
|
||||
className="h-20 sm:h-24 lg:h-32 mx-auto"
|
||||
className={`${getLogoSizeClasses(heroLogoSize)} 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>
|
||||
)}
|
||||
|
||||
{/* Event Title */}
|
||||
{eventName && (
|
||||
@@ -158,6 +182,25 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
</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