fix: resolve external media dimensions, gallery theme race condition, and add email color customization

- Fix dimension repair for external media by using photoResolver instead of hardcoded paths
- Extract photo dimensions via Sharp during external media import
- Remove duplicate theme useEffect from GalleryView to prevent flash/revert race condition
- Pass event welcome_message to Story layout footer for per-event customization
- Add email_primary_color/email_secondary_color settings with admin UI color pickers
- Add i18n keys for email branding in all 4 locales (en, de, ru, pt)
This commit is contained in:
Paul Nothaft
2026-03-16 22:22:36 +01:00
parent 088de43f09
commit bbeedd1888
13 changed files with 220 additions and 92 deletions
@@ -16,7 +16,6 @@ import { UserPhotoUpload } from './UserPhotoUpload';
import type { FilterType } from './GalleryFilter';
import { analyticsService } from '../../services/analytics.service';
import { useDevToolsProtection } from '../../hooks/useDevToolsProtection';
import { GALLERY_THEME_PRESETS } from '../../types/theme.types';
import { api } from '../../config/api';
import { Upload, Menu } from 'lucide-react';
import { galleryService } from '../../services/gallery.service';
@@ -44,7 +43,7 @@ interface GalleryViewProps {
export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const { t } = useTranslation();
const { logout } = useGalleryAuth();
const { setTheme, theme } = useTheme();
const { theme } = useTheme();
const [selectedCategoryId, setSelectedCategoryId] = useState<number | string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size' | 'rating' | 'capture_date'>('date');
@@ -275,62 +274,6 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
setStaticHeroPhoto(defaultHeroPhoto);
}, [selectedCategoryId, data?.categories, data?.photos, defaultHeroPhoto]);
// Apply theme when settings are loaded
useEffect(() => {
if (settingsData && data?.event) {
let themeToApply = null;
const fullEvent = data.event; // Use the full event data from API
if (fullEvent.color_theme) {
try {
// Check if it's a valid JSON string
if (fullEvent.color_theme.startsWith('{')) {
const eventTheme = JSON.parse(fullEvent.color_theme);
themeToApply = eventTheme;
} else {
// Handle legacy theme names - check if it's a preset
const preset = GALLERY_THEME_PRESETS[fullEvent.color_theme];
if (preset) {
themeToApply = preset.config;
} else {
// Unknown theme name, fall back to global theme
if (settingsData.theme_config) {
themeToApply = settingsData.theme_config;
}
}
}
} catch {
// Invalid theme format - use default
// Fall back to global theme
if (settingsData.theme_config) {
themeToApply = settingsData.theme_config;
}
}
} else if (settingsData.theme_config) {
// No event theme, use global theme
themeToApply = settingsData.theme_config;
}
// Apply theme with a small delay to ensure it overrides any global theme
if (themeToApply) {
// Use setTimeout to ensure this runs after any global theme application
const timer = setTimeout(() => {
// If there's a hero photo, add it to gallery settings
if (fullEvent.hero_photo_id && themeToApply.gallerySettings) {
themeToApply.gallerySettings.heroImageId = fullEvent.hero_photo_id;
// Apply hero photo ID to existing gallery settings
} else if (fullEvent.hero_photo_id) {
themeToApply.gallerySettings = { heroImageId: fullEvent.hero_photo_id };
// Create gallery settings with hero photo ID
}
setTheme(themeToApply);
}, 0);
return () => clearTimeout(timer);
}
}
}, [settingsData, data, setTheme]); // Use data instead of event prop
// Calculate days until expiration (null means never expires)
const daysUntilExpiration = event.expires_at
? differenceInDays(parseISO(event.expires_at), new Date())
@@ -619,6 +562,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
headerStyle={data?.event?.header_style || theme.headerStyle}
heroDividerStyle={data?.event?.hero_divider_style || theme.heroDividerStyle || 'wave'}
heroImageAnchor={data?.event?.hero_image_anchor || 'center'}
welcomeMessage={event.welcome_message}
onLogout={logout}
/>
@@ -794,6 +738,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
headerStyle={data?.event?.header_style || theme.headerStyle}
heroDividerStyle={data?.event?.hero_divider_style || theme.heroDividerStyle || 'wave'}
heroImageAnchor={data?.event?.hero_image_anchor || 'center'}
welcomeMessage={event.welcome_message}
/>
</div>
@@ -64,6 +64,8 @@ interface PhotoGridWithLayoutsProps {
heroDividerStyle?: HeroDividerStyle;
// Hero image anchor position (#162) keyword or "X% Y%" focal point
heroImageAnchor?: string;
// Welcome message (per-event) for layouts that display it
welcomeMessage?: string;
// Logout callback for full-page layouts
onLogout?: () => void;
}
@@ -97,6 +99,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
headerStyle,
heroDividerStyle = 'wave',
heroImageAnchor = 'center',
welcomeMessage,
onLogout
}) => {
const { t } = useTranslation();
@@ -226,6 +229,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
heroLogoVisible,
heroLogoSize,
heroLogoPosition,
welcomeMessage,
onLogout,
};
@@ -34,6 +34,7 @@ interface CategoryScene {
interface GalleryStoryLayoutProps extends BaseGalleryLayoutProps {
heroPhotoOverride?: Photo | null;
welcomeMessage?: string;
}
export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
@@ -55,6 +56,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
feedbackEnabled = false,
feedbackOptions,
heroPhotoOverride,
welcomeMessage,
onLogout
}) => {
// These props are passed by parent but we use our own feedback system, so mark as intentionally unused
@@ -348,7 +350,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
<footer className="story-footer">
<h2 className="story-footer-title">{t('gallery.thankYou', 'Thank You')}</h2>
<p className="story-footer-text">
{t('gallery.thankYouMessage', 'For being part of our story and making our special day unforgettable.')}
{welcomeMessage || t('gallery.thankYouMessage', 'For being part of our story and making our special day unforgettable.')}
</p>
{allowDownloads && (
<button className="story-footer-btn" onClick={handleDownloadAll}>