fix(branding): unify hero logo SIZE the same way as visibility (#756)
Follow-on to the visibility fix in this PR — hero_logo_size had the same split-brain: GalleryLayout read the global branding_logo_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 the global size didn't reach hero-header galleries. Now mirrored on the visibility model: NULL per-event hero_logo_size = inherit branding_logo_size; explicit = override. - Migration 153: hero_logo_size nullable + backfill NULL so existing galleries inherit the global size (restores GalleryLayout's prior live-global behaviour and fixes the hero-header staleness). - Creation stores NULL unless explicit; gallery.js resolves per-event ?? global and sends the effective size. - GalleryLayout now consumes that resolved size for the hero logo (new heroLogoSize prop) instead of the global — both render paths match. - Admin size control gains a 'Use branding default' (inherit) option. Verified: migration on SQLite + PG; live resolution (inherit follows global both ways, override wins); creation stores NULL on PG; tsc clean, 106 adminEvents+gallery tests pass, build green.
This commit is contained in:
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -95,7 +95,7 @@ module.exports = (router) => {
|
|||||||
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
||||||
// Hero logo settings
|
// Hero logo settings
|
||||||
body('hero_logo_visible').optional().isBoolean(),
|
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']),
|
body('hero_logo_position').optional().isIn(['top', 'center', 'bottom']),
|
||||||
// Header style settings (decoupled from layout)
|
// Header style settings (decoupled from layout)
|
||||||
body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']),
|
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
|
const effectiveHeroLogoVisible = req.body.hero_logo_visible !== undefined
|
||||||
? formatBoolean(hero_logo_visible)
|
? formatBoolean(hero_logo_visible)
|
||||||
: null;
|
: 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;
|
const effectiveHeroLogoPosition = req.body.hero_logo_position || brandingDefaults.hero_logo_position;
|
||||||
|
|
||||||
// Inherit "Detect dev tools" from the global Image Security setting unless
|
// 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(),
|
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
||||||
// Hero logo settings
|
// Hero logo settings
|
||||||
body('hero_logo_visible').optional().isBoolean(),
|
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']),
|
body('hero_logo_position').optional().isIn(['top', 'center', 'bottom']),
|
||||||
// Header style settings (decoupled from layout)
|
// Header style settings (decoupled from layout)
|
||||||
body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']),
|
body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']),
|
||||||
|
|||||||
@@ -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 requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0');
|
||||||
const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true);
|
const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true);
|
||||||
|
const globalLogoSize = await getAppSetting('branding_logo_size', 'medium');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
event_name: event.event_name,
|
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',
|
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',
|
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_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_position: event.hero_logo_position || 'top',
|
||||||
hero_logo_url: event.hero_logo_url || null,
|
hero_logo_url: event.hero_logo_url || null,
|
||||||
header_style: event.header_style || 'standard',
|
header_style: event.header_style || 'standard',
|
||||||
@@ -649,6 +651,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res)
|
|||||||
// one switch controls both surfaces.
|
// one switch controls both surfaces.
|
||||||
const useOriginalFilenames = await getUseOriginalFilenames();
|
const useOriginalFilenames = await getUseOriginalFilenames();
|
||||||
const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true);
|
const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true);
|
||||||
|
const globalLogoSize = await getAppSetting('branding_logo_size', 'medium');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
event: {
|
event: {
|
||||||
@@ -668,7 +671,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res)
|
|||||||
enable_devtools_protection: req.event.enable_devtools_protection === true,
|
enable_devtools_protection: req.event.enable_devtools_protection === true,
|
||||||
use_canvas_rendering: req.event.use_canvas_rendering === true,
|
use_canvas_rendering: req.event.use_canvas_rendering === true,
|
||||||
hero_logo_visible: resolveHeroLogoVisible(req.event.hero_logo_visible, globalHeroLogoVisible),
|
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_position: req.event.hero_logo_position || 'top',
|
||||||
hero_logo_url: req.event.hero_logo_url || null,
|
hero_logo_url: req.event.hero_logo_url || null,
|
||||||
header_style: req.event.header_style || 'standard',
|
header_style: req.event.header_style || 'standard',
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ interface GalleryLayoutProps {
|
|||||||
// backend (per-event override, else the global branding toggle) (#756).
|
// backend (per-event override, else the global branding toggle) (#756).
|
||||||
// When provided it wins over brandingSettings.logo_display_hero.
|
// When provided it wins over brandingSettings.logo_display_hero.
|
||||||
heroLogoVisible?: boolean;
|
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?: {
|
brandingSettings?: {
|
||||||
company_name?: string;
|
company_name?: string;
|
||||||
company_tagline?: string;
|
company_tagline?: string;
|
||||||
@@ -113,6 +118,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
event,
|
event,
|
||||||
brandingSettings,
|
brandingSettings,
|
||||||
heroLogoVisible,
|
heroLogoVisible,
|
||||||
|
heroLogoSize,
|
||||||
showLogout = false,
|
showLogout = false,
|
||||||
onLogout,
|
onLogout,
|
||||||
showDownloadAll = false,
|
showDownloadAll = false,
|
||||||
@@ -162,7 +168,10 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
|
|
||||||
// Calculate logo size classes based on settings
|
// Calculate logo size classes based on settings
|
||||||
const getLogoDimensions = (context: 'header' | 'hero'): { className: string; style?: React.CSSProperties } => {
|
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;
|
const maxHeight = brandingSettings?.logo_max_height || 48;
|
||||||
|
|
||||||
if (size === 'custom') {
|
if (size === 'custom') {
|
||||||
@@ -221,7 +230,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const headerLogoSize = getLogoDimensions('header');
|
const headerLogoSize = getLogoDimensions('header');
|
||||||
const heroLogoSize = getLogoDimensions('hero');
|
const heroLogoDimensions = getLogoDimensions('hero');
|
||||||
|
|
||||||
// Footer overhaul (#441 + #440). All five socials are independent;
|
// Footer overhaul (#441 + #440). All five socials are independent;
|
||||||
// empty string = hide just that icon. Per-event promo override:
|
// empty string = hide just that icon. Per-event promo override:
|
||||||
@@ -607,9 +616,9 @@ 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={`${heroLogoSize.className} w-auto object-contain mx-auto`}
|
className={`${heroLogoDimensions.className} w-auto object-contain mx-auto`}
|
||||||
style={{
|
style={{
|
||||||
...(heroLogoSize.style || {}),
|
...(heroLogoDimensions.style || {}),
|
||||||
// Only apply brightness/invert filter to default logo; custom logos display as-is
|
// Only apply brightness/invert filter to default logo; custom logos display as-is
|
||||||
filter: brandLogoUrl
|
filter: brandLogoUrl
|
||||||
? 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
|
? 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
|
||||||
|
|||||||
@@ -821,6 +821,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
}}
|
}}
|
||||||
brandingSettings={brandingSettings}
|
brandingSettings={brandingSettings}
|
||||||
heroLogoVisible={data?.event?.hero_logo_visible !== false}
|
heroLogoVisible={data?.event?.hero_logo_visible !== false}
|
||||||
|
heroLogoSize={data?.event?.hero_logo_size || undefined}
|
||||||
headerStyle={data?.event?.header_style || theme.headerStyle}
|
headerStyle={data?.event?.header_style || theme.headerStyle}
|
||||||
showLogout={true}
|
showLogout={true}
|
||||||
onLogout={logout}
|
onLogout={logout}
|
||||||
|
|||||||
@@ -309,7 +309,8 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
// Load hero logo settings from event. Preserve null = "inherit global"
|
// Load hero logo settings from event. Preserve null = "inherit global"
|
||||||
// (#756) — don't collapse it to true, or saving would snapshot an override.
|
// (#756) — don't collapse it to true, or saving would snapshot an override.
|
||||||
hero_logo_visible: event.hero_logo_visible ?? null,
|
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_logo_position: event.hero_logo_position || 'top',
|
||||||
// Hero image anchor position (#162)
|
// Hero image anchor position (#162)
|
||||||
hero_image_anchor: event.hero_image_anchor || 'center',
|
hero_image_anchor: event.hero_image_anchor || 'center',
|
||||||
|
|||||||
@@ -620,10 +620,12 @@ export const EventInformationCard: React.FC<EventInformationCardProps> = ({
|
|||||||
{t('events.heroLogoSize', 'Logo Size')}
|
{t('events.heroLogoSize', 'Logo Size')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={editForm.hero_logo_size}
|
// '' = inherit the global branding logo size (#756).
|
||||||
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_size: e.target.value as 'small' | 'medium' | 'large' | 'xlarge' }))}
|
value={editForm.hero_logo_size ?? ''}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_size: e.target.value === '' ? null : e.target.value as 'small' | 'medium' | 'large' | 'xlarge' }))}
|
||||||
className="w-full sm:w-48 px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md shadow-sm focus:ring-primary-500 focus:border-accent-dark text-sm"
|
className="w-full sm:w-48 px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md shadow-sm focus:ring-primary-500 focus:border-accent-dark text-sm"
|
||||||
>
|
>
|
||||||
|
<option value="">{t('events.heroLogoInherit', 'Use branding default')}</option>
|
||||||
<option value="small">{t('events.heroLogoSizeSmall', 'Small')}</option>
|
<option value="small">{t('events.heroLogoSizeSmall', 'Small')}</option>
|
||||||
<option value="medium">{t('events.heroLogoSizeMedium', 'Medium')}</option>
|
<option value="medium">{t('events.heroLogoSizeMedium', 'Medium')}</option>
|
||||||
<option value="large">{t('events.heroLogoSizeLarge', 'Large')}</option>
|
<option value="large">{t('events.heroLogoSizeLarge', 'Large')}</option>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export type EditFormState = {
|
|||||||
use_canvas_rendering: boolean;
|
use_canvas_rendering: boolean;
|
||||||
// Hero logo settings. null = inherit the global branding toggle (#756).
|
// Hero logo settings. null = inherit the global branding toggle (#756).
|
||||||
hero_logo_visible: boolean | null;
|
hero_logo_visible: boolean | null;
|
||||||
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge';
|
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge' | null;
|
||||||
hero_logo_position: 'top' | 'center' | 'bottom';
|
hero_logo_position: 'top' | 'center' | 'bottom';
|
||||||
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
||||||
hero_image_anchor: string;
|
hero_image_anchor: string;
|
||||||
@@ -74,7 +74,7 @@ export const INITIAL_EDIT_FORM: EditFormState = {
|
|||||||
use_canvas_rendering: false,
|
use_canvas_rendering: false,
|
||||||
// Hero logo settings — null = inherit global branding toggle (#756)
|
// Hero logo settings — null = inherit global branding toggle (#756)
|
||||||
hero_logo_visible: null,
|
hero_logo_visible: null,
|
||||||
hero_logo_size: 'medium',
|
hero_logo_size: null,
|
||||||
hero_logo_position: 'top',
|
hero_logo_position: 'top',
|
||||||
// Hero image anchor position (#162)
|
// Hero image anchor position (#162)
|
||||||
hero_image_anchor: 'center',
|
hero_image_anchor: 'center',
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export interface Event {
|
|||||||
use_canvas_rendering?: boolean;
|
use_canvas_rendering?: boolean;
|
||||||
// Hero logo customization fields
|
// Hero logo customization fields
|
||||||
hero_logo_visible?: boolean | null;
|
hero_logo_visible?: boolean | null;
|
||||||
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge';
|
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge' | null;
|
||||||
hero_logo_position?: 'top' | 'center' | 'bottom';
|
hero_logo_position?: 'top' | 'center' | 'bottom';
|
||||||
hero_logo_url?: string | null;
|
hero_logo_url?: string | null;
|
||||||
// Per-event opt-in for using the hero photo as the social-share
|
// Per-event opt-in for using the hero photo as the social-share
|
||||||
@@ -189,7 +189,7 @@ export interface GalleryData {
|
|||||||
overlay_protection?: boolean;
|
overlay_protection?: boolean;
|
||||||
// Hero logo customization fields
|
// Hero logo customization fields
|
||||||
hero_logo_visible?: boolean | null;
|
hero_logo_visible?: boolean | null;
|
||||||
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge';
|
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge' | null;
|
||||||
hero_logo_position?: 'top' | 'center' | 'bottom';
|
hero_logo_position?: 'top' | 'center' | 'bottom';
|
||||||
hero_logo_url?: string | null;
|
hero_logo_url?: string | null;
|
||||||
// Header style settings (decoupled from layout)
|
// Header style settings (decoupled from layout)
|
||||||
|
|||||||
Reference in New Issue
Block a user