diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 1385a138..8b0211cf 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -544,9 +544,16 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl return res.status(400).json({ error: 'No logo file uploaded' }); } + // ?variant=dark stores a separate dark-mode logo (branding_logo_*_dark); + // anything else is the default (light) logo. Consumers pick the dark + // variant when the active theme is dark, falling back to the light one. + const isDark = req.query.variant === 'dark' || req.body.variant === 'dark'; + const pathKey = isDark ? 'branding_logo_path_dark' : 'branding_logo_path'; + const urlKey = isDark ? 'branding_logo_url_dark' : 'branding_logo_url'; + // Get old logo to delete const oldLogoSetting = await db('app_settings') - .where('setting_key', 'branding_logo_path') + .where('setting_key', pathKey) .first(); if (oldLogoSetting && oldLogoSetting.setting_value) { @@ -568,7 +575,7 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl await db('app_settings') .insert({ - setting_key: 'branding_logo_path', + setting_key: pathKey, setting_value: JSON.stringify(logoPath), setting_type: 'branding', updated_at: new Date() @@ -582,7 +589,7 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl // Save public URL await db('app_settings') .insert({ - setting_key: 'branding_logo_url', + setting_key: urlKey, setting_value: JSON.stringify(publicPath), setting_type: 'branding', updated_at: new Date() @@ -603,6 +610,36 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl } }); +// Remove a logo. ?variant=dark clears the dark-mode logo +// (branding_logo_*_dark); otherwise the default logo. Best-effort file +// unlink, then blanks the url + path settings. +router.delete('/logo', adminAuth, requirePermission('settings.edit'), async (req, res) => { + try { + const isDark = req.query.variant === 'dark'; + const pathKey = isDark ? 'branding_logo_path_dark' : 'branding_logo_path'; + const urlKey = isDark ? 'branding_logo_url_dark' : 'branding_logo_url'; + + const pathSetting = await db('app_settings').where('setting_key', pathKey).first(); + if (pathSetting && pathSetting.setting_value) { + try { + let p = pathSetting.setting_value; + if (p.startsWith('"')) p = JSON.parse(p); + await fs.unlink(p); + } catch (error) { + console.error('Failed to delete logo file:', error); + } + } + await db('app_settings') + .whereIn('setting_key', [pathKey, urlKey]) + .update({ setting_value: JSON.stringify(''), updated_at: new Date() }); + + res.json({ message: 'Logo removed' }); + } catch (error) { + console.error('Logo delete error:', error); + res.status(500).json({ error: 'Failed to remove logo' }); + } +}); + // Upload watermark logo router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.edit'), upload.single('watermarkLogo'), async (req, res) => { try { diff --git a/backend/src/routes/publicSettings.js b/backend/src/routes/publicSettings.js index dc1f7178..050adc9e 100644 --- a/backend/src/routes/publicSettings.js +++ b/backend/src/routes/publicSettings.js @@ -59,6 +59,7 @@ 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_url_dark: settingsObject.branding_logo_url_dark || '', 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', diff --git a/frontend/src/components/admin/AdminHeader.tsx b/frontend/src/components/admin/AdminHeader.tsx index ff3efaaa..0315bd77 100644 --- a/frontend/src/components/admin/AdminHeader.tsx +++ b/frontend/src/components/admin/AdminHeader.tsx @@ -33,7 +33,10 @@ export const AdminHeader: React.FC = ({ onMenuClick }) => { const { data: brandingSettings } = usePublicSettings(); const companyName = brandingSettings?.branding_company_name?.trim() || 'PicPeak'; - const logoUrl = brandingSettings?.branding_logo_url?.trim(); + // Dark-mode logo variant: use it when the admin theme is dark and a dark + // logo was uploaded; otherwise fall back to the light logo. + const logoUrl = (isDark && brandingSettings?.branding_logo_url_dark?.trim()) + || brandingSettings?.branding_logo_url?.trim(); const logoDisplayMode = brandingSettings?.branding_logo_display_mode || 'logo_and_text'; // Logo placement honours the same Branding > Logo Position setting // the gallery does. 'sidepanel' moves the logo into the AdminSidebar diff --git a/frontend/src/components/gallery/GalleryLayout.tsx b/frontend/src/components/gallery/GalleryLayout.tsx index 97b2a75b..c4bb7d03 100644 --- a/frontend/src/components/gallery/GalleryLayout.tsx +++ b/frontend/src/components/gallery/GalleryLayout.tsx @@ -32,6 +32,7 @@ interface GalleryLayoutProps { footer_text?: string; favicon_url?: string; logo_url?: string; + logo_url_dark?: string; logo_size?: 'small' | 'medium' | 'large' | 'xlarge' | 'custom'; logo_max_height?: number; logo_position?: 'left' | 'center' | 'right' | 'sidepanel'; @@ -122,6 +123,9 @@ export const GalleryLayout: React.FC = ({ const { t } = useTranslation(); const { format } = useLocalizedDate(); const { theme } = useTheme(); + // Dark-mode logo variant: prefer it on dark gallery themes, else light. + const brandLogoUrl = (theme.colorMode === 'dark' && brandingSettings?.logo_url_dark) + || brandingSettings?.logo_url; const guestIdentity = useGuestIdentityOptional(); // Footer legal-link config. Cached aggressively because the toggle state @@ -304,8 +308,8 @@ export const GalleryLayout: React.FC = ({ {shouldShowLogo('header') && (
{brandingSettings?.company_name = ({ {/* Logo - Show custom logo or fallback to PicPeak logo */} {shouldShowLogo('hero') && (
- {brandingSettings?.company_name = ({ slug, event }) => { footer_text: settingsData.branding_footer_text || '', watermark_enabled: settingsData.branding_watermark_enabled || false, logo_url: settingsData.branding_logo_url || null, + logo_url_dark: settingsData.branding_logo_url_dark || null, logo_size: settingsData.branding_logo_size || 'medium', logo_max_height: settingsData.branding_logo_max_height || 48, logo_position: settingsData.branding_logo_position || 'left', diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 2f38fb2a..7c371084 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1682,6 +1682,8 @@ "logo": "Logo", "uploadLogo": "Logo hochladen", "logoHelp": "Empfohlene Größe: 200x60px, PNG oder JPEG", + "logoDark": "Logo für Dunkelmodus", + "logoDarkHelp": "Optional. Wird bei dunklen Designs / im Dunkelmodus angezeigt; fällt auf das Hauptlogo zurück, wenn nicht gesetzt.", "favicon": "Favicon", "currentFavicon": "Aktuelles Favicon", "uploadFavicon": "Favicon hochladen", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 8398ddc1..e6279e33 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1271,6 +1271,8 @@ "logo": "Logo", "uploadLogo": "Upload Logo", "logoHelp": "Recommended size: 200x60px, PNG or JPEG", + "logoDark": "Dark-mode logo", + "logoDarkHelp": "Optional. Shown on dark themes / dark mode; falls back to the main logo when unset.", "favicon": "Favicon", "currentFavicon": "Current favicon", "uploadFavicon": "Upload Favicon", diff --git a/frontend/src/pages/admin/BrandingPage.tsx b/frontend/src/pages/admin/BrandingPage.tsx index 59c4db72..b619437b 100644 --- a/frontend/src/pages/admin/BrandingPage.tsx +++ b/frontend/src/pages/admin/BrandingPage.tsx @@ -12,6 +12,7 @@ import { buildResourceUrl } from '../../utils/url'; import { useFeatureEnabled, useFeatureFlags } from '../../contexts/FeatureFlagsContext'; import { CustomerDashboardBrandingCard } from '../../components/admin/CustomerDashboardBrandingCard'; import { PdfTypographyCard } from '../../components/admin/PdfTypographyCard'; +import { usePublicSettings } from '../../hooks/usePublicSettings'; export const BrandingPage: React.FC = () => { const { t } = useTranslation(); @@ -260,6 +261,47 @@ export const BrandingPage: React.FC = () => { }); }; + // Dark-mode logo — self-contained (the upload endpoint persists + // branding_logo_url_dark directly; not part of the theme payload). + // Consumers (admin header, gallery) pick it when the theme is dark. + const { data: pubSettings } = usePublicSettings(); + const [logoDarkUrl, setLogoDarkUrl] = useState(''); + useEffect(() => { + if (pubSettings?.branding_logo_url_dark !== undefined) { + setLogoDarkUrl(pubSettings.branding_logo_url_dark || ''); + } + }, [pubSettings?.branding_logo_url_dark]); + + const refreshSettings = () => { + queryClient.invalidateQueries({ queryKey: ['public-settings'] }); + queryClient.invalidateQueries({ queryKey: ['admin-settings'] }); + }; + + const handleDarkLogoUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + try { + const url = await settingsService.uploadLogo(file, 'dark'); + setLogoDarkUrl(url); + refreshSettings(); + toast.success(t('toast.uploadSuccess')); + } catch (error) { + console.error('Failed to upload dark logo:', error); + toast.error(t('toast.uploadError')); + } + }; + + const handleRemoveDarkLogo = async () => { + try { + await settingsService.removeLogo('dark'); + setLogoDarkUrl(''); + refreshSettings(); + } catch (error) { + console.error('Failed to remove dark logo:', error); + toast.error(t('toast.saveError')); + } + }; + const handleWatermarkLogoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -621,6 +663,50 @@ export const BrandingPage: React.FC = () => { {t('branding.logoHelp', 'PNG, JPG or SVG format, recommended width: 200px')}

+ {/* Dark-mode logo */} +
+ +
+ {logoDarkUrl && ( +
+ Dark logo + +
+ )} +
+ + +
+
+

+ {t('branding.logoDarkHelp', 'Optional. Shown on dark themes / dark mode; falls back to the main logo when unset.')} +

+
{/* Logo Size */}