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
|
# Local SQLite files in backend
|
||||||
backend/*.sqlite*
|
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('disable_right_click').optional().isBoolean(),
|
||||||
body('watermark_downloads').optional().isBoolean(),
|
body('watermark_downloads').optional().isBoolean(),
|
||||||
body('watermark_text').optional().trim(),
|
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) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
logger.debug('Create event request body', { body: req.body });
|
logger.debug('Create event request body', { body: req.body });
|
||||||
@@ -197,7 +201,11 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
moderate_comments = true,
|
moderate_comments = true,
|
||||||
show_feedback_to_guests = true,
|
show_feedback_to_guests = true,
|
||||||
// CSS Template
|
// 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;
|
} = req.body;
|
||||||
|
|
||||||
const customerName = getCustomerNameFromPayload(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_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false),
|
||||||
watermark_text,
|
watermark_text,
|
||||||
require_password: formatBoolean(requirePassword),
|
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');
|
}).returning('id');
|
||||||
|
|
||||||
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
|
// 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;
|
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) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
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());
|
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
|
// Update event
|
||||||
await db('events')
|
await db('events')
|
||||||
.where('id', id)
|
.where('id', id)
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ router.get('/:slug/info', async (req, res) => {
|
|||||||
'require_password',
|
'require_password',
|
||||||
'color_theme',
|
'color_theme',
|
||||||
'enable_devtools_protection',
|
'enable_devtools_protection',
|
||||||
'use_canvas_rendering'
|
'use_canvas_rendering',
|
||||||
|
'hero_logo_visible',
|
||||||
|
'hero_logo_size',
|
||||||
|
'hero_logo_position'
|
||||||
)
|
)
|
||||||
.first();
|
.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_downloads: event.watermark_downloads === true || event.watermark_downloads === 1 || event.watermark_downloads === '1',
|
||||||
watermark_text: event.watermark_text,
|
watermark_text: event.watermark_text,
|
||||||
enable_devtools_protection: event.enable_devtools_protection === true || event.enable_devtools_protection === 1 || event.enable_devtools_protection === '1',
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching gallery info:', 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,
|
watermark_text: req.event.watermark_text,
|
||||||
enable_devtools_protection: req.event.enable_devtools_protection === true,
|
enable_devtools_protection: req.event.enable_devtools_protection === true,
|
||||||
use_canvas_rendering: req.event.use_canvas_rendering === 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
|
...protectionSettings
|
||||||
},
|
},
|
||||||
categories: categories,
|
categories: categories,
|
||||||
|
|||||||
@@ -689,6 +689,9 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
disableRightClick={disableRightClick}
|
disableRightClick={disableRightClick}
|
||||||
enableDevtoolsProtection={enableDevtoolsProtection}
|
enableDevtoolsProtection={enableDevtoolsProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
useCanvasRendering={useCanvasRendering}
|
||||||
|
heroLogoVisible={data?.event?.hero_logo_visible !== false}
|
||||||
|
heroLogoSize={data?.event?.hero_logo_size || 'medium'}
|
||||||
|
heroLogoPosition={data?.event?.hero_logo_position || 'top'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ interface PhotoGridWithLayoutsProps {
|
|||||||
requireNameEmail?: boolean;
|
requireNameEmail?: boolean;
|
||||||
};
|
};
|
||||||
onFeedbackChange?: () => void;
|
onFeedbackChange?: () => void;
|
||||||
|
// Hero logo customization options
|
||||||
|
heroLogoVisible?: boolean;
|
||||||
|
heroLogoSize?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
|
heroLogoPosition?: 'top' | 'center' | 'bottom';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
||||||
@@ -76,7 +80,10 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
|||||||
eventName,
|
eventName,
|
||||||
eventLogo,
|
eventLogo,
|
||||||
eventDate,
|
eventDate,
|
||||||
expiresAt
|
expiresAt,
|
||||||
|
heroLogoVisible = true,
|
||||||
|
heroLogoSize = 'medium',
|
||||||
|
heroLogoPosition = 'top'
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -200,6 +207,9 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
|||||||
expiresAt,
|
expiresAt,
|
||||||
feedbackEnabled,
|
feedbackEnabled,
|
||||||
feedbackOptions,
|
feedbackOptions,
|
||||||
|
heroLogoVisible,
|
||||||
|
heroLogoSize,
|
||||||
|
heroLogoPosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
let LayoutComponent;
|
let LayoutComponent;
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
|||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
// Use a static hero photo independent of current filter
|
// Use a static hero photo independent of current filter
|
||||||
heroPhotoOverride?: Photo | null;
|
heroPhotoOverride?: Photo | null;
|
||||||
|
// Hero logo customization options
|
||||||
|
heroLogoVisible?: boolean;
|
||||||
|
heroLogoSize?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
|
heroLogoPosition?: 'top' | 'center' | 'bottom';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||||
@@ -34,6 +38,9 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
|||||||
eventDate,
|
eventDate,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
heroPhotoOverride,
|
heroPhotoOverride,
|
||||||
|
heroLogoVisible = true,
|
||||||
|
heroLogoSize = 'medium',
|
||||||
|
heroLogoPosition = 'top',
|
||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
protectionLevel = 'standard',
|
protectionLevel = 'standard',
|
||||||
useEnhancedProtection = false,
|
useEnhancedProtection = false,
|
||||||
@@ -51,6 +58,22 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
|||||||
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
|
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
|
||||||
const gallerySettings = theme.gallerySettings || {};
|
const gallerySettings = theme.gallerySettings || {};
|
||||||
const overlayOpacity = gallerySettings.heroOverlayOpacity || 0.3;
|
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 [likedIds, setLikedIds] = useState<Set<number>>(new Set());
|
||||||
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
|
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
|
||||||
const gridRef = useRef<HTMLDivElement | null>(null);
|
const gridRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -133,31 +156,51 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
|||||||
{/* Hero Content */}
|
{/* Hero Content */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-center px-4">
|
<div className="text-center px-4">
|
||||||
{/* Logo - Show custom logo or fallback to PicPeak logo */}
|
{/* Logo at top position */}
|
||||||
<div className="mb-6">
|
{heroLogoVisible && heroLogoPosition === 'top' && (
|
||||||
<img
|
<div className="mb-6">
|
||||||
src={eventLogo ?
|
<img
|
||||||
buildResourceUrl(eventLogo) :
|
src={eventLogo ?
|
||||||
'/picpeak-logo-transparent.png'
|
buildResourceUrl(eventLogo) :
|
||||||
}
|
'/picpeak-logo-transparent.png'
|
||||||
alt="Event logo"
|
}
|
||||||
className="h-20 sm:h-24 lg:h-32 mx-auto"
|
alt="Event logo"
|
||||||
style={{
|
className={`${getLogoSizeClasses(heroLogoSize)} mx-auto`}
|
||||||
// Only apply brightness/invert filter to default logo; custom logos display as-is
|
style={{
|
||||||
filter: eventLogo
|
filter: eventLogo
|
||||||
? 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
|
? '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))'
|
: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Event Title */}
|
{/* Event Title */}
|
||||||
{eventName && (
|
{eventName && (
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-white drop-shadow-lg mb-4">
|
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-white drop-shadow-lg mb-4">
|
||||||
{eventName}
|
{eventName}
|
||||||
</h1>
|
</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 */}
|
{/* Event Dates */}
|
||||||
{(eventDate || expiresAt) && (
|
{(eventDate || expiresAt) && (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 text-white/90">
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -833,6 +833,22 @@
|
|||||||
"protectionLevelStandard": "Standard - Tastenkombinationen blockiert",
|
"protectionLevelStandard": "Standard - Tastenkombinationen blockiert",
|
||||||
"protectionLevelEnhanced": "Erweitert - Bildschirmfoto-Erkennung",
|
"protectionLevelEnhanced": "Erweitert - Bildschirmfoto-Erkennung",
|
||||||
"protectionLevelMaximum": "Maximum - DevTools-Erkennung & Canvas-Rendering",
|
"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",
|
"heroPhoto": "Hero-Foto",
|
||||||
"heroPhotoHelp": "Wählen Sie ein hervorgehobenes Foto für das Hero-Galerie-Layout",
|
"heroPhotoHelp": "Wählen Sie ein hervorgehobenes Foto für das Hero-Galerie-Layout",
|
||||||
"selectHeroPhoto": "Hero-Foto auswählen",
|
"selectHeroPhoto": "Hero-Foto auswählen",
|
||||||
|
|||||||
@@ -459,6 +459,22 @@
|
|||||||
"protectionLevelStandard": "Standard - Keyboard shortcuts blocked",
|
"protectionLevelStandard": "Standard - Keyboard shortcuts blocked",
|
||||||
"protectionLevelEnhanced": "Enhanced - Print screen detection",
|
"protectionLevelEnhanced": "Enhanced - Print screen detection",
|
||||||
"protectionLevelMaximum": "Maximum - DevTools detection & canvas rendering",
|
"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",
|
"heroPhoto": "Hero Photo",
|
||||||
"heroPhotoHelp": "Select a featured photo for the hero gallery layout",
|
"heroPhotoHelp": "Select a featured photo for the hero gallery layout",
|
||||||
"selectHeroPhoto": "Select Hero Photo",
|
"selectHeroPhoto": "Select Hero Photo",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
Monitor,
|
Monitor,
|
||||||
Droplets,
|
Droplets,
|
||||||
MousePointer
|
MousePointer,
|
||||||
|
Layout
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { parseISO, differenceInDays, isValid } from 'date-fns';
|
import { parseISO, differenceInDays, isValid } from 'date-fns';
|
||||||
|
|
||||||
@@ -160,6 +161,10 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
watermark_downloads: boolean;
|
watermark_downloads: boolean;
|
||||||
enable_devtools_protection: boolean;
|
enable_devtools_protection: boolean;
|
||||||
use_canvas_rendering: 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);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
@@ -184,6 +189,10 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
watermark_downloads: false,
|
watermark_downloads: false,
|
||||||
enable_devtools_protection: true,
|
enable_devtools_protection: true,
|
||||||
use_canvas_rendering: false,
|
use_canvas_rendering: false,
|
||||||
|
// Hero logo settings
|
||||||
|
hero_logo_visible: true,
|
||||||
|
hero_logo_size: 'medium',
|
||||||
|
hero_logo_position: 'top',
|
||||||
});
|
});
|
||||||
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
|
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
|
||||||
feedback_enabled: false,
|
feedback_enabled: false,
|
||||||
@@ -406,6 +415,10 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
watermark_downloads: event.watermark_downloads ?? false,
|
watermark_downloads: event.watermark_downloads ?? false,
|
||||||
enable_devtools_protection: event.enable_devtools_protection ?? true,
|
enable_devtools_protection: event.enable_devtools_protection ?? true,
|
||||||
use_canvas_rendering: event.use_canvas_rendering ?? false,
|
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);
|
setShowNewPassword(false);
|
||||||
@@ -496,6 +509,10 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
watermark_downloads: editForm.watermark_downloads,
|
watermark_downloads: editForm.watermark_downloads,
|
||||||
enable_devtools_protection: editForm.enable_devtools_protection,
|
enable_devtools_protection: editForm.enable_devtools_protection,
|
||||||
use_canvas_rendering: editForm.use_canvas_rendering,
|
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
|
// Only include fields that have defined values
|
||||||
@@ -1062,6 +1079,66 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<dl className="space-y-4">
|
<dl className="space-y-4">
|
||||||
@@ -1197,6 +1274,37 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</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>
|
</dl>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export interface Event {
|
|||||||
watermark_downloads?: boolean;
|
watermark_downloads?: boolean;
|
||||||
enable_devtools_protection?: boolean;
|
enable_devtools_protection?: boolean;
|
||||||
use_canvas_rendering?: 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 {
|
export interface GalleryInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user