fix: theme save without Live Preview, Branding default on new events, gallery loading flicker (#323, #321)

#323-A — Branding colour changes weren't persisting unless "Apply changes
immediately (Live Preview)" was checked. ThemeCustomizerEnhanced was
gating its `onChange` callback on `isPreviewMode`, but the parent
BrandingPage already gates global `setTheme()` on its own copy of that
flag — so the customizer's gate was double-gating and silently dropped
the new values from the parent state that Save reads from. Always
propagate `onChange`; let parents decide what's "live". Removed the now
no-op `isPreviewMode` prop and dropped the unused passers.

#323-B — Default theme set in Branding wasn't applied to new events.
CreateEventPage only inherited the event-type's recommended preset, with
'default' falling back to Classic Grid. Now reads `settings.theme_config`
on first load and uses it as the form's starting theme; the event-type
effect skips the generic 'default' so the Branding default sticks for
event types like "Other".

#321 — Visitors saw four sequential render states when opening a gallery
(full-page "Loading Gallery" → "publicly accessible — loading photos"
card → skeleton grid → real gallery). Extracted the skeleton into a
shared <GallerySkeleton/> and used it for both GalleryView's photos-
loading state and GalleryPage's gallery-info-loading + public-auto-login
phases. The "publicly accessible" interstitial is gone. Net: one
continuous skeleton from URL open until real photos render.
This commit is contained in:
Paul Nothaft
2026-04-27 22:38:00 +02:00
parent 63a6bfebce
commit 822be9a9b2
6 changed files with 124 additions and 117 deletions
@@ -12,7 +12,6 @@ interface ThemeCustomizerEnhancedProps {
onChange: (theme: ThemeConfig) => void;
presetName?: string;
onPresetChange?: (presetName: string) => void;
isPreviewMode?: boolean;
showGalleryLayouts?: boolean;
hideActions?: boolean;
onApply?: (theme: ThemeConfig, metadata: { presetName: string }) => Promise<void> | void;
@@ -76,7 +75,6 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
onChange,
presetName = 'default',
onPresetChange,
isPreviewMode = false,
showGalleryLayouts = true,
hideActions = false,
onApply,
@@ -125,10 +123,11 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
onPresetChange('custom');
}
if (isPreviewMode) {
// Include customCss in the propagated theme
onChange({ ...updated, customCss });
}
// Always propagate to parent so Save sees the latest values (#323).
// The "Apply changes immediately (Live Preview)" toggle controls whether
// the parent applies the theme globally — that gating belongs in the
// parent, not here.
onChange({ ...updated, customCss });
};
const handlePresetSelect = (presetKey: string) => {
@@ -140,9 +139,8 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
if (onPresetChange) {
onPresetChange(presetKey);
}
if (isPreviewMode) {
onChange(preset.config);
}
// Always propagate; live-apply gating is the parent's concern (#323).
onChange(preset.config);
}
};
@@ -795,7 +793,7 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
mutedTextColor: '#a3a3a3',
};
setLocalTheme(updated);
if (isPreviewMode) onChange({ ...updated, customCss });
onChange({ ...updated, customCss });
} else if (mode === 'light' && localTheme.colorMode === 'dark') {
const updated = {
...localTheme,
@@ -807,7 +805,7 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
mutedTextColor: '#737373',
};
setLocalTheme(updated);
if (isPreviewMode) onChange({ ...updated, customCss });
onChange({ ...updated, customCss });
}
}}
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
@@ -1181,10 +1179,8 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
setSelectedPreset('custom');
onPresetChange('custom');
}
// Propagate customCss changes to parent in preview mode
if (isPreviewMode) {
onChange({ ...localTheme, customCss: newCss });
}
// Propagate to parent so Save sees the latest CSS (#323).
onChange({ ...localTheme, customCss: newCss });
}}
placeholder="/* Add custom CSS here */"
className="w-full h-40 px-3 py-2 font-mono text-sm border border-neutral-300 dark:border-neutral-600 rounded-lg bg-neutral-50 dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100"
@@ -0,0 +1,31 @@
import React from 'react';
import { Skeleton, SkeletonGalleryGrid } from '../common';
/**
* Loading placeholder shown while a gallery is resolving (slug → info →
* auto-login → photos). Used by GalleryPage during the pre-photos phases and
* by GalleryView while the photos query runs, so the visitor sees one
* continuous skeleton instead of multiple full-page interstitials (#321).
*/
export const GallerySkeleton: React.FC = () => (
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background, #fafafa)' }}>
<header className="bg-surface border-b border-surface sticky top-0 z-40">
<div className="container py-4">
<div className="flex items-center justify-between">
<div>
<Skeleton height={32} width={200} className="mb-2" />
<Skeleton height={20} width={300} />
</div>
<div className="flex items-center gap-2">
<Skeleton height={40} width={120} />
<Skeleton height={40} width={100} />
</div>
</div>
</div>
</header>
<div className="container mt-6">
<Skeleton height={80} className="mb-6" />
<SkeletonGalleryGrid count={12} />
</div>
</div>
);
@@ -3,7 +3,8 @@ import { differenceInDays, parseISO } from 'date-fns';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Button, SkeletonGalleryGrid, Skeleton } from '../common';
import { Button } from '../common';
import { GallerySkeleton } from './GallerySkeleton';
import { useGalleryAuth, useTheme } from '../../contexts';
import { useGalleryPhotos, useDownloadAllPhotos } from '../../hooks/useGallery';
import { PhotoGridWithLayouts } from './PhotoGridWithLayouts';
@@ -587,31 +588,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
}, [showUrgentWarning, daysUntilExpiration, slug]);
if (isLoading) {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background)' }}>
{/* Header Skeleton */}
<header className="bg-surface border-b border-surface sticky top-0 z-40">
<div className="container py-4">
<div className="flex items-center justify-between">
<div>
<Skeleton height={32} width={200} className="mb-2" />
<Skeleton height={20} width={300} />
</div>
<div className="flex items-center gap-2">
<Skeleton height={40} width={120} />
<Skeleton height={40} width={100} />
</div>
</div>
</div>
</header>
{/* Content Skeleton */}
<div className="container mt-6">
<Skeleton height={80} className="mb-6" />
<SkeletonGalleryGrid count={12} />
</div>
</div>
);
return <GallerySkeleton />;
}
if (error || !data) {