feat: add configurable animated background for landing pages

Extract BackgroundConfigEditor as reusable controlled component from
BackgroundEditor. Add background settings section to landing editor.
Render per-landing backgrounds on public purchase pages.

- Extract BackgroundConfigEditor (value/onChange) from BackgroundEditor
- Refactor BackgroundEditor to thin wrapper with API persistence
- Add StaticBackgroundRenderer for prop-based config (public pages)
- Add background_config to landing API types and editor form
- Render animated background on QuickPurchase page
This commit is contained in:
Fringg
2026-03-07 12:46:05 +03:00
parent 887b13dec2
commit a404690334
7 changed files with 374 additions and 329 deletions

View File

@@ -96,52 +96,35 @@ function reduceMobileSettings(settings: Record<string, unknown>): Record<string,
return reduced;
}
export function BackgroundRenderer() {
/** Renders the background animation into a portal at document.body */
function RenderBackground({ config }: { config: AnimationConfig }) {
const prefersReducedMotion = useMemo(
() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
[],
);
const { data: config } = useQuery({
queryKey: ['animation-config'],
queryFn: async () => {
const raw = await brandingApi.getAnimationConfig();
// Validate and clamp API response same as localStorage
const result = validateConfig(raw) ?? DEFAULT_ANIMATION_CONFIG;
setCachedConfig(result);
return result;
},
initialData: getCachedConfig() ?? undefined,
initialDataUpdatedAt: 0,
staleTime: 30_000,
});
const effectiveConfig = config ?? DEFAULT_ANIMATION_CONFIG;
if (!effectiveConfig.enabled || effectiveConfig.type === 'none' || prefersReducedMotion) {
if (!config.enabled || config.type === 'none' || prefersReducedMotion) {
return null;
}
const bgType = effectiveConfig.type as Exclude<BackgroundType, 'none'>;
const bgType = config.type as Exclude<BackgroundType, 'none'>;
const Component = backgroundComponents[bgType];
if (!Component) return null;
const isMobile = window.innerWidth < 768;
const settings =
effectiveConfig.reducedOnMobile && isMobile
? reduceMobileSettings(effectiveConfig.settings)
: effectiveConfig.settings;
config.reducedOnMobile && isMobile ? reduceMobileSettings(config.settings) : config.settings;
// On mobile, cap blur to 4px max — full blur is extremely GPU-heavy
const effectiveBlur = isMobile ? Math.min(effectiveConfig.blur, 4) : effectiveConfig.blur;
const effectiveBlur = isMobile ? Math.min(config.blur, 4) : config.blur;
return createPortal(
<div
className="pointer-events-none fixed inset-0"
style={{
zIndex: -2,
opacity: effectiveConfig.opacity,
opacity: config.opacity,
filter: effectiveBlur > 0 ? `blur(${effectiveBlur}px)` : undefined,
contain: 'strict',
backfaceVisibility: 'hidden',
@@ -154,3 +137,29 @@ export function BackgroundRenderer() {
document.body,
);
}
/** BackgroundRenderer that fetches config from the branding API (for authenticated app shell) */
export function BackgroundRenderer() {
const { data: config } = useQuery({
queryKey: ['animation-config'],
queryFn: async () => {
const raw = await brandingApi.getAnimationConfig();
const result = validateConfig(raw) ?? DEFAULT_ANIMATION_CONFIG;
setCachedConfig(result);
return result;
},
initialData: getCachedConfig() ?? undefined,
initialDataUpdatedAt: 0,
staleTime: 30_000,
});
const effectiveConfig = config ?? DEFAULT_ANIMATION_CONFIG;
return <RenderBackground config={effectiveConfig} />;
}
/** StaticBackgroundRenderer that uses a provided config (for public landing pages) */
export function StaticBackgroundRenderer({ config }: { config: AnimationConfig }) {
const validated = useMemo(() => validateConfig(config), [config]);
if (!validated) return null;
return <RenderBackground config={validated} />;
}