diff --git a/backend/migrations/core/153_hero_logo_size_inherit.js b/backend/migrations/core/153_hero_logo_size_inherit.js new file mode 100644 index 00000000..cebbf8c2 --- /dev/null +++ b/backend/migrations/core/153_hero_logo_size_inherit.js @@ -0,0 +1,51 @@ +/** + * Migration 153: make events.hero_logo_size NULL-able so NULL means "inherit + * the global branding_logo_size" (#756 follow-up — the size counterpart of 152). + * + * Before: hero_logo_size was `varchar NOT NULL DEFAULT 'medium'`, snapshotted + * from the global branding_logo_size at creation. The two gallery render paths + * then disagreed — GalleryLayout read the global size live, while the + * hero-header path used the per-event snapshot — so a hero logo could render at + * different sizes on different layouts, and changing the global size didn't + * update hero-header galleries. + * + * After: NULL = inherit. gallery read-resolution falls back to + * branding_logo_size when the per-event value is NULL, and both render paths + * consume that resolved size. + * + * Data backfill: NULL out ALL existing hero_logo_size so every gallery inherits + * the global size going forward. Unlike a boolean we can't tell a defaulted + * value from a chosen one — but nulling is the safe choice here: it restores the + * live-global behaviour GalleryLayout already had, and the per-event size can be + * re-set from the event's edit page. + */ +exports.up = async function (knex) { + if (!(await knex.schema.hasColumn('events', 'hero_logo_size'))) return; + + const client = (knex.client.config.client || '').toLowerCase(); + if (client === 'pg' || client === 'postgresql') { + await knex.raw('ALTER TABLE events ALTER COLUMN hero_logo_size DROP DEFAULT'); + await knex.raw('ALTER TABLE events ALTER COLUMN hero_logo_size DROP NOT NULL'); + } else { + await knex.schema.alterTable('events', (t) => { + t.string('hero_logo_size', 20).nullable().alter(); + }); + } + + await knex('events').update({ hero_logo_size: null }); +}; + +exports.down = async function (knex) { + if (!(await knex.schema.hasColumn('events', 'hero_logo_size'))) return; + await knex('events').whereNull('hero_logo_size').update({ hero_logo_size: 'medium' }); + + const client = (knex.client.config.client || '').toLowerCase(); + if (client === 'pg' || client === 'postgresql') { + await knex.raw("ALTER TABLE events ALTER COLUMN hero_logo_size SET DEFAULT 'medium'"); + await knex.raw('ALTER TABLE events ALTER COLUMN hero_logo_size SET NOT NULL'); + } else { + await knex.schema.alterTable('events', (t) => { + t.string('hero_logo_size', 20).notNullable().defaultTo('medium').alter(); + }); + } +}; diff --git a/backend/src/routes/adminEvents/crud.js b/backend/src/routes/adminEvents/crud.js index 0cdb4c1a..238c4456 100644 --- a/backend/src/routes/adminEvents/crud.js +++ b/backend/src/routes/adminEvents/crud.js @@ -95,7 +95,7 @@ module.exports = (router) => { 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_size').optional({ nullable: true }).isIn(['small', 'medium', 'large', 'xlarge']), body('hero_logo_position').optional().isIn(['top', 'center', 'bottom']), // Header style settings (decoupled from layout) body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']), @@ -346,7 +346,9 @@ module.exports = (router) => { const effectiveHeroLogoVisible = req.body.hero_logo_visible !== undefined ? formatBoolean(hero_logo_visible) : null; - const effectiveHeroLogoSize = req.body.hero_logo_size || brandingDefaults.hero_logo_size; + // NULL = inherit the global branding_logo_size (#756), resolved at read + // time. Only an explicit per-event size overrides it. + const effectiveHeroLogoSize = req.body.hero_logo_size || null; const effectiveHeroLogoPosition = req.body.hero_logo_position || brandingDefaults.hero_logo_position; // Inherit "Detect dev tools" from the global Image Security setting unless @@ -1223,7 +1225,7 @@ module.exports = (router) => { 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_size').optional({ nullable: true }).isIn(['small', 'medium', 'large', 'xlarge']), body('hero_logo_position').optional().isIn(['top', 'center', 'bottom']), // Header style settings (decoupled from layout) body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']), diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 54ce3f2b..ba62bf1e 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -195,6 +195,7 @@ router.get('/:slug/info', async (req, res) => { const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0'); const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true); + const globalLogoSize = await getAppSetting('branding_logo_size', 'medium'); res.json({ event_name: event.event_name, @@ -213,7 +214,8 @@ router.get('/:slug/info', async (req, res) => { 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', hero_logo_visible: resolveHeroLogoVisible(event.hero_logo_visible, globalHeroLogoVisible), - hero_logo_size: event.hero_logo_size || 'medium', + // #756: NULL per-event size inherits the global branding_logo_size. + hero_logo_size: event.hero_logo_size || globalLogoSize || 'medium', hero_logo_position: event.hero_logo_position || 'top', hero_logo_url: event.hero_logo_url || null, header_style: event.header_style || 'standard', @@ -649,6 +651,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res) // one switch controls both surfaces. const useOriginalFilenames = await getUseOriginalFilenames(); const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true); + const globalLogoSize = await getAppSetting('branding_logo_size', 'medium'); res.json({ event: { @@ -668,7 +671,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res) enable_devtools_protection: req.event.enable_devtools_protection === true, use_canvas_rendering: req.event.use_canvas_rendering === true, hero_logo_visible: resolveHeroLogoVisible(req.event.hero_logo_visible, globalHeroLogoVisible), - hero_logo_size: req.event.hero_logo_size || 'medium', + hero_logo_size: req.event.hero_logo_size || globalLogoSize || 'medium', hero_logo_position: req.event.hero_logo_position || 'top', hero_logo_url: req.event.hero_logo_url || null, header_style: req.event.header_style || 'standard', diff --git a/frontend/src/components/gallery/GalleryLayout.tsx b/frontend/src/components/gallery/GalleryLayout.tsx index c9e8ba26..60d8e064 100644 --- a/frontend/src/components/gallery/GalleryLayout.tsx +++ b/frontend/src/components/gallery/GalleryLayout.tsx @@ -29,6 +29,11 @@ interface GalleryLayoutProps { // backend (per-event override, else the global branding toggle) (#756). // When provided it wins over brandingSettings.logo_display_hero. heroLogoVisible?: boolean; + // Effective hero-logo SIZE, resolved the same way (per-event override, else + // the global branding_logo_size) (#756). When provided it wins over + // brandingSettings.logo_size for the hero logo — so both render paths + // (this layout and the hero-header) size the logo identically. + heroLogoSize?: 'small' | 'medium' | 'large' | 'xlarge' | 'custom'; brandingSettings?: { company_name?: string; company_tagline?: string; @@ -113,6 +118,7 @@ export const GalleryLayout: React.FC = ({ event, brandingSettings, heroLogoVisible, + heroLogoSize, showLogout = false, onLogout, showDownloadAll = false, @@ -162,7 +168,10 @@ export const GalleryLayout: React.FC = ({ // Calculate logo size classes based on settings const getLogoDimensions = (context: 'header' | 'hero'): { className: string; style?: React.CSSProperties } => { - const size = brandingSettings?.logo_size || 'medium'; + // #756: the hero logo uses the backend-resolved per-event size (override, + // else global) so it matches the hero-header layout; the header logo keeps + // the global size. + const size = (context === 'hero' && heroLogoSize) ? heroLogoSize : (brandingSettings?.logo_size || 'medium'); const maxHeight = brandingSettings?.logo_max_height || 48; if (size === 'custom') { @@ -221,7 +230,7 @@ export const GalleryLayout: React.FC = ({ }; const headerLogoSize = getLogoDimensions('header'); - const heroLogoSize = getLogoDimensions('hero'); + const heroLogoDimensions = getLogoDimensions('hero'); // Footer overhaul (#441 + #440). All five socials are independent; // empty string = hide just that icon. Per-event promo override: @@ -607,9 +616,9 @@ export const GalleryLayout: React.FC = ({ '/picpeak-logo-transparent.png' } alt={brandingSettings?.company_name || 'PicPeak'} - className={`${heroLogoSize.className} w-auto object-contain mx-auto`} + className={`${heroLogoDimensions.className} w-auto object-contain mx-auto`} style={{ - ...(heroLogoSize.style || {}), + ...(heroLogoDimensions.style || {}), // Only apply brightness/invert filter to default logo; custom logos display as-is filter: brandLogoUrl ? 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))' diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 8d7201a8..b63eca31 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -821,6 +821,7 @@ export const GalleryView: React.FC = ({ slug, event }) => { }} brandingSettings={brandingSettings} heroLogoVisible={data?.event?.hero_logo_visible !== false} + heroLogoSize={data?.event?.hero_logo_size || undefined} headerStyle={data?.event?.header_style || theme.headerStyle} showLogout={true} onLogout={logout} diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 84c852a1..ac3fd15a 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -309,7 +309,8 @@ export const EventDetailsPage: React.FC = () => { // Load hero logo settings from event. Preserve null = "inherit global" // (#756) — don't collapse it to true, or saving would snapshot an override. hero_logo_visible: event.hero_logo_visible ?? null, - hero_logo_size: event.hero_logo_size || 'medium', + // Preserve null = "inherit global size" (#756) — don't collapse to medium. + hero_logo_size: event.hero_logo_size ?? null, hero_logo_position: event.hero_logo_position || 'top', // Hero image anchor position (#162) hero_image_anchor: event.hero_image_anchor || 'center', diff --git a/frontend/src/pages/admin/event-details/EventInformationCard.tsx b/frontend/src/pages/admin/event-details/EventInformationCard.tsx index 3353f937..24290e85 100644 --- a/frontend/src/pages/admin/event-details/EventInformationCard.tsx +++ b/frontend/src/pages/admin/event-details/EventInformationCard.tsx @@ -620,10 +620,12 @@ export const EventInformationCard: React.FC = ({ {t('events.heroLogoSize', 'Logo Size')}