feat: implement gallery logo customization (Issue #17)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||||
|
};
|
||||||
@@ -170,7 +170,13 @@ router.put('/branding', adminAuth, async (req, res) => {
|
|||||||
watermark_size,
|
watermark_size,
|
||||||
favicon_url,
|
favicon_url,
|
||||||
logo_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;
|
} = req.body;
|
||||||
|
|
||||||
const brandingSettings = {
|
const brandingSettings = {
|
||||||
@@ -184,7 +190,13 @@ router.put('/branding', adminAuth, async (req, res) => {
|
|||||||
watermark_size,
|
watermark_size,
|
||||||
favicon_url,
|
favicon_url,
|
||||||
logo_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
|
// Handle favicon deletion if empty string or null is provided
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ router.get('/', async (req, res) => {
|
|||||||
branding_watermark_size: settingsObject.branding_watermark_size || 15,
|
branding_watermark_size: settingsObject.branding_watermark_size || 15,
|
||||||
branding_favicon_url: settingsObject.branding_favicon_url || '',
|
branding_favicon_url: settingsObject.branding_favicon_url || '',
|
||||||
branding_logo_url: settingsObject.branding_logo_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,
|
theme_config: settingsObject.theme_config || null,
|
||||||
default_language: settingsObject.general_default_language || 'en',
|
default_language: settingsObject.general_default_language || 'en',
|
||||||
enable_analytics: settingsObject.general_enable_analytics !== false,
|
enable_analytics: settingsObject.general_enable_analytics !== false,
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ interface GalleryLayoutProps {
|
|||||||
footer_text?: string;
|
footer_text?: string;
|
||||||
favicon_url?: string;
|
favicon_url?: string;
|
||||||
logo_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;
|
showLogout?: boolean;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
@@ -56,6 +62,53 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
const fontFamily = theme.fontFamily || 'Inter, sans-serif';
|
const fontFamily = theme.fontFamily || 'Inter, sans-serif';
|
||||||
const headingFontFamily = theme.headingFontFamily || fontFamily;
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-neutral-50">
|
<div className="min-h-screen bg-neutral-50">
|
||||||
{/* Dynamic Favicon */}
|
{/* Dynamic Favicon */}
|
||||||
@@ -122,16 +175,31 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Logo - Show custom logo or fallback to PicPeak logo */}
|
{/* Logo - Show custom logo or fallback to PicPeak logo */}
|
||||||
<div className="flex-shrink-0">
|
{shouldShowLogo('header') && (
|
||||||
|
<div className={`flex-shrink-0 flex items-center gap-2 ${brandingSettings?.logo_position === 'center' ? 'flex-1' : ''} ${getLogoPositionClass()}`}>
|
||||||
<img
|
<img
|
||||||
src={brandingSettings?.logo_url ?
|
src={brandingSettings?.logo_url ?
|
||||||
buildResourceUrl(brandingSettings.logo_url) :
|
buildResourceUrl(brandingSettings.logo_url) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||||
className="h-8 sm:h-10 lg:h-12 w-auto object-contain"
|
className={`${typeof getLogoSizeClass('header') === 'string' ? getLogoSizeClass('header') : ''} w-auto object-contain`}
|
||||||
|
style={typeof getLogoSizeClass('header') === 'object' ? getLogoSizeClass('header') : undefined}
|
||||||
/>
|
/>
|
||||||
|
{shouldShowCompanyName() && brandingSettings?.company_name && (
|
||||||
|
<span className="hidden sm:inline text-lg font-semibold text-neutral-900">
|
||||||
|
{brandingSettings.company_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{!shouldShowLogo('header') && shouldShowCompanyName() && brandingSettings?.company_name && (
|
||||||
|
<div className={`flex-shrink-0 ${brandingSettings?.logo_position === 'center' ? 'flex-1' : ''} ${getLogoPositionClass()}`}>
|
||||||
|
<span className="text-lg font-semibold text-neutral-900">
|
||||||
|
{brandingSettings.company_name || 'PicPeak'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center - Event info */}
|
{/* Center - Event info */}
|
||||||
@@ -277,6 +345,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
<div className="container py-12 sm:py-16 lg:py-20 relative z-10">
|
<div className="container py-12 sm:py-16 lg:py-20 relative z-10">
|
||||||
<div className="text-center max-w-4xl mx-auto">
|
<div className="text-center max-w-4xl mx-auto">
|
||||||
{/* Logo - Show custom logo or fallback to PicPeak logo */}
|
{/* Logo - Show custom logo or fallback to PicPeak logo */}
|
||||||
|
{shouldShowLogo('hero') && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<img
|
<img
|
||||||
src={brandingSettings?.logo_url ?
|
src={brandingSettings?.logo_url ?
|
||||||
@@ -284,12 +353,24 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||||
className="h-16 sm:h-20 lg:h-24 w-auto object-contain mx-auto"
|
className={`${typeof getLogoSizeClass('hero') === 'string' ? getLogoSizeClass('hero') : ''} w-auto object-contain mx-auto`}
|
||||||
style={{
|
style={typeof getLogoSizeClass('hero') === 'object' ?
|
||||||
filter: 'brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
|
{ ...getLogoSizeClass('hero'), filter: 'brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))' } :
|
||||||
}}
|
{ filter: 'brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))' }
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
{shouldShowCompanyName() && brandingSettings?.company_name && (
|
||||||
|
<div className="mt-3 text-xl sm:text-2xl font-semibold text-white/90" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)' }}>
|
||||||
|
{brandingSettings.company_name}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!shouldShowLogo('hero') && shouldShowCompanyName() && brandingSettings?.company_name && (
|
||||||
|
<div className="mb-6 text-2xl sm:text-3xl font-bold text-white" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)' }}>
|
||||||
|
{brandingSettings.company_name || 'PicPeak'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Event Name */}
|
{/* Event Name */}
|
||||||
<h1
|
<h1
|
||||||
|
|||||||
@@ -144,6 +144,21 @@ export const BrandingPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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<HTMLInputElement>) => {
|
const handleWatermarkLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -316,6 +331,166 @@ export const BrandingPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Logo Customization Settings */}
|
||||||
|
<div className="mt-6 pt-6 border-t border-neutral-200">
|
||||||
|
<h3 className="text-md font-semibold text-neutral-900 mb-4">{t('branding.logoCustomization', 'Logo Customization')}</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Logo Upload */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
||||||
|
{t('branding.logo', 'Logo')}
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{brandingSettings.logo_url && (
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={brandingSettings.logo_url.startsWith('http') ? brandingSettings.logo_url : buildResourceUrl(brandingSettings.logo_url)}
|
||||||
|
alt="Logo"
|
||||||
|
className="h-16 object-contain bg-neutral-100 rounded p-2"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleBrandingChange('logo_url', '')}
|
||||||
|
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-600"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<label className="cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg,image/svg+xml"
|
||||||
|
onChange={handleLogoUpload}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<span className="btn-secondary inline-flex items-center">
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
{brandingSettings.logo_url ? t('branding.changeLogo', 'Change Logo') : t('branding.uploadLogo', 'Upload Logo')}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-600 mt-1">
|
||||||
|
{t('branding.logoHelp', 'PNG, JPG or SVG format, recommended width: 200px')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Logo Size */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
||||||
|
{t('branding.logoSize', 'Logo Size')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={brandingSettings.logo_size || 'medium'}
|
||||||
|
onChange={(e) => handleBrandingChange('logo_size', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
<option value="small">{t('branding.logoSizeSmall', 'Small (32px)')}</option>
|
||||||
|
<option value="medium">{t('branding.logoSizeMedium', 'Medium (48px)')}</option>
|
||||||
|
<option value="large">{t('branding.logoSizeLarge', 'Large (64px)')}</option>
|
||||||
|
<option value="xlarge">{t('branding.logoSizeXLarge', 'Extra Large (96px)')}</option>
|
||||||
|
<option value="custom">{t('branding.logoSizeCustom', 'Custom')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Height (only shown when size is custom) */}
|
||||||
|
{brandingSettings.logo_size === 'custom' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
||||||
|
{t('branding.logoMaxHeight', 'Maximum Height (pixels)')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="20"
|
||||||
|
max="200"
|
||||||
|
value={brandingSettings.logo_max_height || 48}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-neutral-600 mt-1">
|
||||||
|
{t('branding.logoMaxHeightHelp', 'Set a custom maximum height for the logo (20-200 pixels)')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logo Position */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
||||||
|
{t('branding.logoPosition', 'Logo Position in Header')}
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(['left', 'center', 'right'] as const).map((position) => (
|
||||||
|
<button
|
||||||
|
key={position}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleBrandingChange('logo_position', position)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
brandingSettings.logo_position === position
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t(`branding.position${position.charAt(0).toUpperCase() + position.slice(1)}`, position.charAt(0).toUpperCase() + position.slice(1))}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Display Mode */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
||||||
|
{t('branding.logoDisplayMode', 'Display Mode')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={brandingSettings.logo_display_mode || 'logo_and_text'}
|
||||||
|
onChange={(e) => handleBrandingChange('logo_display_mode', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
<option value="logo_only">{t('branding.logoOnly', 'Logo Only')}</option>
|
||||||
|
<option value="text_only">{t('branding.textOnly', 'Company Name Only')}</option>
|
||||||
|
<option value="logo_and_text">{t('branding.logoAndText', 'Logo and Company Name')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Display Options */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={brandingSettings.logo_display_header !== false}
|
||||||
|
onChange={(e) => handleBrandingChange('logo_display_header', e.target.checked)}
|
||||||
|
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-neutral-900">
|
||||||
|
{t('branding.showLogoInHeader', 'Show logo in gallery header')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-neutral-600">
|
||||||
|
{t('branding.showLogoInHeaderHelp', 'Display the logo in the main header bar')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={brandingSettings.logo_display_hero !== false}
|
||||||
|
onChange={(e) => handleBrandingChange('logo_display_hero', e.target.checked)}
|
||||||
|
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-neutral-900">
|
||||||
|
{t('branding.showLogoInHero', 'Show logo in hero section')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-neutral-600">
|
||||||
|
{t('branding.showLogoInHeroHelp', 'Display the logo in hero sections (for non-grid layouts)')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t border-neutral-200">
|
<div className="mt-6 pt-6 border-t border-neutral-200">
|
||||||
<label className="flex items-center gap-3 cursor-pointer">
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export interface BrandingSettings {
|
|||||||
watermark_logo_url?: string;
|
watermark_logo_url?: string;
|
||||||
logo_url?: string;
|
logo_url?: string;
|
||||||
favicon_url?: string;
|
favicon_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';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeSettings {
|
export interface ThemeSettings {
|
||||||
@@ -215,7 +221,13 @@ export const settingsService = {
|
|||||||
watermark_size: rawSettings.branding_watermark_size || 15,
|
watermark_size: rawSettings.branding_watermark_size || 15,
|
||||||
watermark_logo_url: rawSettings.branding_watermark_logo_url || undefined,
|
watermark_logo_url: rawSettings.branding_watermark_logo_url || undefined,
|
||||||
logo_url: rawSettings.branding_logo_url || undefined,
|
logo_url: rawSettings.branding_logo_url || undefined,
|
||||||
favicon_url: rawSettings.branding_favicon_url || undefined
|
favicon_url: rawSettings.branding_favicon_url || undefined,
|
||||||
|
logo_size: rawSettings.branding_logo_size || 'medium',
|
||||||
|
logo_max_height: rawSettings.branding_logo_max_height || 48,
|
||||||
|
logo_position: rawSettings.branding_logo_position || 'left',
|
||||||
|
logo_display_header: rawSettings.branding_logo_display_header !== false,
|
||||||
|
logo_display_hero: rawSettings.branding_logo_display_hero !== false,
|
||||||
|
logo_display_mode: rawSettings.branding_logo_display_mode || 'logo_and_text'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user