diff --git a/backend/migrations/core/086_migrate_non_grid_standard_to_banner.js b/backend/migrations/core/086_migrate_non_grid_standard_to_banner.js new file mode 100644 index 00000000..dcc9f29f --- /dev/null +++ b/backend/migrations/core/086_migrate_non_grid_standard_to_banner.js @@ -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`); +}; diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index d57bcb4b..e3da5a61 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -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), diff --git a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx index 274337f8..cd8ce1b7 100644 --- a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx +++ b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Palette, RotateCcw, Check, Layout, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid, ChevronDown, Code, Info, FileCode, ImageIcon, Minimize2, EyeOff, Menu, SlidersHorizontal, Columns, Film, AlertTriangle } from 'lucide-react'; +import { Palette, RotateCcw, Check, Layout, LayoutTemplate, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid, ChevronDown, Code, Info, FileCode, ImageIcon, Minimize2, EyeOff, Menu, SlidersHorizontal, Columns, Film, AlertTriangle } from 'lucide-react'; import { Button, Card, Input } from '../common'; import { ThemeConfig, GALLERY_THEME_PRESETS, GalleryLayoutType, HeaderStyleType, HeroDividerStyle } from '../../types/theme.types'; import type { EnabledTemplate } from '../../services/cssTemplates.service'; @@ -35,6 +35,7 @@ const layoutIcons: Record = { const headerStyleIcons: Record = { hero: , standard: , + banner: , minimal: , none: }; @@ -626,7 +627,7 @@ export const ThemeCustomizerEnhanced: React.FC = (

{t('branding.headerStyleDescription', 'Choose how the gallery header appears. The header style is independent of the photo layout.')}

-
+
{(Object.keys(headerStyleIcons) as HeaderStyleType[]).map((style) => ( - )} - - {/* Logout button */} - {showLogout && onLogout && ( - - )} -
-
- - - )} - - {/* For grid layout - everything in one bar (standard header) */} - {!isNonGridLayout && !isHeroHeader && !isMinimalHeader && !isNoHeader && ( +
+ {/* Standard / Banner header - full bar with logo, event info, and actions (all layouts) */} + {!isHeroHeader && !isMinimalHeader && !isNoHeader && (
{/* Left side - Menu button, Logo */} @@ -322,56 +274,8 @@ export const GalleryLayout: React.FC = ({
)} - {/* For minimal/none header + non-grid layouts - compact menu bar */} - {isNonGridLayout && (isMinimalHeader || isNoHeader) && ( -
-
-
-
- {menuButton} - {headerExtra} - {isMinimalHeader && ( -

- {event.event_name} -

- )} -
-
- {showDownloadAll && onDownloadAll && ( - - )} - {showLogout && onLogout && ( - - )} -
-
-
-
- )} - - {/* For minimal header + grid layout - compact bar with event name */} - {!isNonGridLayout && isMinimalHeader && ( + {/* Minimal header - compact bar with event name (all layouts) */} + {isMinimalHeader && (
@@ -413,8 +317,8 @@ export const GalleryLayout: React.FC = ({
)} - {/* For none header + grid layout - just functional buttons, no event info */} - {!isNonGridLayout && isNoHeader && ( + {/* No-header style - just functional buttons, no event info (all layouts) */} + {isNoHeader && (
@@ -495,8 +399,8 @@ export const GalleryLayout: React.FC = ({ )}
- {/* Colored banner for non-grid layouts when using standard header style */} - {isNonGridLayout && !isHeroHeader && !isMinimalHeader && !isNoHeader && ( + {/* Colored banner — only when headerStyle === 'banner', regardless of layout */} + {isBannerHeader && (
= { carouselInterval: 5000, carouselShowThumbnails: true }, - headerStyle: 'standard', + headerStyle: 'banner', footerStyle: 'standard', backgroundPattern: 'dots' }, @@ -225,7 +225,7 @@ export const GALLERY_THEME_PRESETS: Record = { timelineGrouping: 'day', timelineShowDates: true }, - headerStyle: 'standard', + headerStyle: 'banner', footerStyle: 'full', buttonStyle: 'outline' },