feat(gallery): decouple header style from layout, add banner option

This commit is contained in:
Luca
2026-05-04 14:34:42 +02:00
parent 4d99eb672d
commit aff29c91bb
7 changed files with 122 additions and 117 deletions
@@ -0,0 +1,96 @@
/**
* Migration: Move existing non-grid + 'standard' events to the new 'banner'
* header style so their visual appearance is preserved.
*
* Until now, GalleryLayout.tsx coupled the colored hero banner to non-grid
* layouts whenever headerStyle was 'standard'. The 'standard' look has been
* decoupled from layout (it now means: compact inline header, no banner) and
* a new 'banner' option has been added that adds the colored banner above
* the standard header.
*
* To keep current galleries looking the same, every event whose effective
* config was non-grid + standard gets migrated to non-grid + banner.
*/
const NON_GRID_LAYOUTS = ['masonry', 'carousel', 'timeline', 'mosaic'];
exports.up = async function(knex) {
console.log('[Migration 086] Migrating non-grid standard headers to banner');
const events = await knex('events')
.where('header_style', 'standard')
.whereNotNull('color_theme')
.select('id', 'color_theme');
let migratedCount = 0;
for (const event of events) {
try {
if (!event.color_theme || !event.color_theme.startsWith('{')) {
continue;
}
const theme = JSON.parse(event.color_theme);
if (!NON_GRID_LAYOUTS.includes(theme.galleryLayout)) {
continue;
}
const updatedTheme = { ...theme, headerStyle: 'banner' };
await knex('events')
.where('id', event.id)
.update({
color_theme: JSON.stringify(updatedTheme),
header_style: 'banner'
});
migratedCount++;
} catch (err) {
console.warn(`[Migration 086] Could not parse color_theme for event ${event.id}: ${err.message}`);
}
}
console.log(`[Migration 086] Migrated ${migratedCount} events from standard to banner`);
};
exports.down = async function(knex) {
console.log('[Migration 086] Reverting non-grid banner headers to standard');
const events = await knex('events')
.where('header_style', 'banner')
.whereNotNull('color_theme')
.select('id', 'color_theme');
let revertedCount = 0;
for (const event of events) {
try {
if (!event.color_theme || !event.color_theme.startsWith('{')) {
continue;
}
const theme = JSON.parse(event.color_theme);
// Only revert rows we would have migrated (non-grid + banner). Leaves
// any banner events that were intentionally created on grid alone.
if (!NON_GRID_LAYOUTS.includes(theme.galleryLayout)) {
continue;
}
const revertedTheme = { ...theme, headerStyle: 'standard' };
await knex('events')
.where('id', event.id)
.update({
color_theme: JSON.stringify(revertedTheme),
header_style: 'standard'
});
revertedCount++;
} catch (err) {
console.warn(`[Migration 086] Could not revert color_theme for event ${event.id}: ${err.message}`);
}
}
console.log(`[Migration 086] Reverted ${revertedCount} events from banner to standard`);
};
+2 -2
View File
@@ -310,7 +310,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
body('hero_logo_size').optional().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', 'minimal', 'none']),
body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']),
body('hero_divider_style').optional().isIn(['wave', 'straight', 'angle', 'curve', 'none']),
// Hero image anchor position (#162) accepts legacy keywords or "X% Y%" focal point
body('hero_image_anchor').optional().custom(validateHeroImageAnchor),
@@ -1016,7 +1016,7 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne
body('hero_logo_size').optional().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', 'minimal', 'none']),
body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']),
body('hero_divider_style').optional().isIn(['wave', 'straight', 'angle', 'curve', 'none']),
// Hero image anchor position (#162) accepts legacy keywords or "X% Y%" focal point
body('hero_image_anchor').optional().custom(validateHeroImageAnchor),