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:
2025-09-02 23:21:49 +02:00
parent 41857ec499
commit 909e760447
6 changed files with 401 additions and 26 deletions
+175
View File
@@ -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 file = e.target.files?.[0];
if (file) {
@@ -316,6 +331,166 @@ export const BrandingPage: React.FC = () => {
</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">
<label className="flex items-center gap-3 cursor-pointer">
<input