From 909e760447c76bb35dbffa553a4665edc5ebccd9 Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 2 Sep 2025 23:21:49 +0200 Subject: [PATCH] feat: implement gallery logo customization (Issue #17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added comprehensive logo customization features for gallery views: - Logo size options (small, medium, large, xlarge, custom) - Logo position control (left, center, right) - Display mode settings (logo only, text only, logo and text) - Visibility controls for header and hero sections - Custom height configuration for fine-tuning Changes: - Added database migration for 6 new logo customization settings - Extended backend APIs to handle logo customization fields - Updated GalleryLayout.tsx with dynamic logo rendering logic - Added logo upload functionality to BrandingPage.tsx - Extended settings service with logo customization types This addresses the issue where the gallery logo was "very large and centered" by providing full control over logo appearance and positioning. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../041_add_logo_customization_settings.js | 89 +++++++++ backend/src/routes/adminSettings.js | 16 +- backend/src/routes/publicSettings.js | 6 + .../src/components/gallery/GalleryLayout.tsx | 127 ++++++++++--- frontend/src/pages/admin/BrandingPage.tsx | 175 ++++++++++++++++++ frontend/src/services/settings.service.ts | 14 +- 6 files changed, 401 insertions(+), 26 deletions(-) create mode 100644 backend/migrations/041_add_logo_customization_settings.js diff --git a/backend/migrations/041_add_logo_customization_settings.js b/backend/migrations/041_add_logo_customization_settings.js new file mode 100644 index 0000000..09358b3 --- /dev/null +++ b/backend/migrations/041_add_logo_customization_settings.js @@ -0,0 +1,89 @@ +exports.up = async function(knex) { + console.log('Running migration: 041_add_logo_customization_settings'); + + // Add default logo customization settings + const logoSettings = [ + { + setting_key: 'branding_logo_size', + setting_value: JSON.stringify('medium'), + setting_type: 'branding', + description: 'Logo size: small, medium, large, xlarge, or custom', + created_at: new Date(), + updated_at: new Date() + }, + { + setting_key: 'branding_logo_max_height', + setting_value: JSON.stringify(48), + setting_type: 'branding', + description: 'Maximum logo height in pixels (used when size is custom)', + created_at: new Date(), + updated_at: new Date() + }, + { + setting_key: 'branding_logo_position', + setting_value: JSON.stringify('left'), + setting_type: 'branding', + description: 'Logo position in header: left, center, right', + created_at: new Date(), + updated_at: new Date() + }, + { + setting_key: 'branding_logo_display_header', + setting_value: JSON.stringify(true), + setting_type: 'branding', + description: 'Show logo in gallery header', + created_at: new Date(), + updated_at: new Date() + }, + { + setting_key: 'branding_logo_display_hero', + setting_value: JSON.stringify(true), + setting_type: 'branding', + description: 'Show logo in hero section (for non-grid layouts)', + created_at: new Date(), + updated_at: new Date() + }, + { + setting_key: 'branding_logo_display_mode', + setting_value: JSON.stringify('logo_and_text'), + setting_type: 'branding', + description: 'Display mode: logo_only, text_only, logo_and_text', + created_at: new Date(), + updated_at: new Date() + } + ]; + + // Insert settings that don't already exist + for (const setting of logoSettings) { + const exists = await knex('app_settings') + .where('setting_key', setting.setting_key) + .first(); + + if (!exists) { + await knex('app_settings').insert(setting); + console.log(`Added setting: ${setting.setting_key}`); + } else { + console.log(`Setting already exists: ${setting.setting_key}`); + } + } + + console.log('Migration 041_add_logo_customization_settings completed'); +}; + +exports.down = async function(knex) { + console.log('Rolling back migration: 041_add_logo_customization_settings'); + + // Remove the logo customization settings + await knex('app_settings') + .whereIn('setting_key', [ + 'branding_logo_size', + 'branding_logo_max_height', + 'branding_logo_position', + 'branding_logo_display_header', + 'branding_logo_display_hero', + 'branding_logo_display_mode' + ]) + .del(); + + console.log('Rollback of 041_add_logo_customization_settings completed'); +}; \ No newline at end of file diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 639c505..8214984 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -170,7 +170,13 @@ router.put('/branding', adminAuth, async (req, res) => { watermark_size, favicon_url, logo_url, - watermark_logo_url + watermark_logo_url, + logo_size, + logo_max_height, + logo_position, + logo_display_header, + logo_display_hero, + logo_display_mode } = req.body; const brandingSettings = { @@ -184,7 +190,13 @@ router.put('/branding', adminAuth, async (req, res) => { watermark_size, favicon_url, logo_url, - watermark_logo_url + watermark_logo_url, + logo_size, + logo_max_height, + logo_position, + logo_display_header, + logo_display_hero, + logo_display_mode }; // Handle favicon deletion if empty string or null is provided diff --git a/backend/src/routes/publicSettings.js b/backend/src/routes/publicSettings.js index 388268f..3bcd6a8 100644 --- a/backend/src/routes/publicSettings.js +++ b/backend/src/routes/publicSettings.js @@ -42,6 +42,12 @@ router.get('/', async (req, res) => { branding_watermark_size: settingsObject.branding_watermark_size || 15, branding_favicon_url: settingsObject.branding_favicon_url || '', branding_logo_url: settingsObject.branding_logo_url || '', + branding_logo_size: settingsObject.branding_logo_size || 'medium', + branding_logo_max_height: settingsObject.branding_logo_max_height || 48, + branding_logo_position: settingsObject.branding_logo_position || 'left', + branding_logo_display_header: settingsObject.branding_logo_display_header !== false, + branding_logo_display_hero: settingsObject.branding_logo_display_hero !== false, + branding_logo_display_mode: settingsObject.branding_logo_display_mode || 'logo_and_text', theme_config: settingsObject.theme_config || null, default_language: settingsObject.general_default_language || 'en', enable_analytics: settingsObject.general_enable_analytics !== false, diff --git a/frontend/src/components/gallery/GalleryLayout.tsx b/frontend/src/components/gallery/GalleryLayout.tsx index c27af56..209ef29 100644 --- a/frontend/src/components/gallery/GalleryLayout.tsx +++ b/frontend/src/components/gallery/GalleryLayout.tsx @@ -23,6 +23,12 @@ interface GalleryLayoutProps { footer_text?: string; favicon_url?: string; logo_url?: string; + logo_size?: 'small' | 'medium' | 'large' | 'xlarge' | 'custom'; + logo_max_height?: number; + logo_position?: 'left' | 'center' | 'right'; + logo_display_header?: boolean; + logo_display_hero?: boolean; + logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text'; }; showLogout?: boolean; onLogout?: () => void; @@ -56,6 +62,53 @@ export const GalleryLayout: React.FC = ({ const fontFamily = theme.fontFamily || 'Inter, sans-serif'; const headingFontFamily = theme.headingFontFamily || fontFamily; + // Calculate logo size classes based on settings + const getLogoSizeClass = (context: 'header' | 'hero') => { + const size = brandingSettings?.logo_size || 'medium'; + const maxHeight = brandingSettings?.logo_max_height || 48; + + if (size === 'custom') { + return { maxHeight: `${maxHeight}px`, height: 'auto' }; + } + + const sizeMap = { + small: context === 'header' ? 'h-6 sm:h-8' : 'h-12 sm:h-14 lg:h-16', + medium: context === 'header' ? 'h-8 sm:h-10 lg:h-12' : 'h-16 sm:h-20 lg:h-24', + large: context === 'header' ? 'h-10 sm:h-12 lg:h-16' : 'h-20 sm:h-24 lg:h-32', + xlarge: context === 'header' ? 'h-12 sm:h-16 lg:h-20' : 'h-24 sm:h-32 lg:h-40' + }; + + return sizeMap[size] || sizeMap.medium; + }; + + // Determine logo position classes + const getLogoPositionClass = () => { + const position = brandingSettings?.logo_position || 'left'; + return { + left: 'justify-start', + center: 'justify-center', + right: 'justify-end' + }[position]; + }; + + // Check if logo should be displayed + const shouldShowLogo = (context: 'header' | 'hero') => { + const displayMode = brandingSettings?.logo_display_mode || 'logo_and_text'; + if (displayMode === 'text_only') return false; + + if (context === 'header') { + return brandingSettings?.logo_display_header !== false; + } else { + return brandingSettings?.logo_display_hero !== false; + } + }; + + // Check if company name should be displayed + const shouldShowCompanyName = () => { + const displayMode = brandingSettings?.logo_display_mode || 'logo_and_text'; + return displayMode !== 'logo_only'; + }; + return (
{/* Dynamic Favicon */} @@ -122,16 +175,31 @@ export const GalleryLayout: React.FC = ({ )} {/* Logo - Show custom logo or fallback to PicPeak logo */} -
- {brandingSettings?.company_name -
+ {shouldShowLogo('header') && ( +
+ {brandingSettings?.company_name + {shouldShowCompanyName() && brandingSettings?.company_name && ( + + {brandingSettings.company_name} + + )} +
+ )} + {!shouldShowLogo('header') && shouldShowCompanyName() && brandingSettings?.company_name && ( +
+ + {brandingSettings.company_name || 'PicPeak'} + +
+ )}
{/* Center - Event info */} @@ -277,19 +345,32 @@ export const GalleryLayout: React.FC = ({
{/* Logo - Show custom logo or fallback to PicPeak logo */} -
- {brandingSettings?.company_name -
+ {shouldShowLogo('hero') && ( +
+ {brandingSettings?.company_name + {shouldShowCompanyName() && brandingSettings?.company_name && ( +
+ {brandingSettings.company_name} +
+ )} +
+ )} + {!shouldShowLogo('hero') && shouldShowCompanyName() && brandingSettings?.company_name && ( +
+ {brandingSettings.company_name || 'PicPeak'} +
+ )} {/* Event Name */}

{ } }; + const handleLogoUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + try { + const logoUrl = await settingsService.uploadLogo(file); + setBrandingSettings(prev => ({ ...prev, logo_url: logoUrl })); + setCurrentTheme(prev => ({ ...prev, logoUrl })); + toast.success(t('toast.uploadSuccess')); + } catch (error) { + console.error('Failed to upload logo:', error); + toast.error(t('toast.uploadError')); + } + } + }; + const handleWatermarkLogoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -316,6 +331,166 @@ export const BrandingPage: React.FC = () => {

+ {/* Logo Customization Settings */} +
+

{t('branding.logoCustomization', 'Logo Customization')}

+ +
+ {/* Logo Upload */} +
+ +
+ {brandingSettings.logo_url && ( +
+ Logo + +
+ )} + +
+

+ {t('branding.logoHelp', 'PNG, JPG or SVG format, recommended width: 200px')} +

+
+ {/* Logo Size */} +
+ + +
+ + {/* Custom Height (only shown when size is custom) */} + {brandingSettings.logo_size === 'custom' && ( +
+ + handleBrandingChange('logo_max_height', parseInt(e.target.value))} + className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500" + /> +

+ {t('branding.logoMaxHeightHelp', 'Set a custom maximum height for the logo (20-200 pixels)')} +

+
+ )} + + {/* Logo Position */} +
+ +
+ {(['left', 'center', 'right'] as const).map((position) => ( + + ))} +
+
+ + {/* Display Mode */} +
+ + +
+ + {/* Display Options */} +
+ + + +
+
+
+