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

@@ -15,6 +15,9 @@ import { tariffsApi, TariffListItem, PeriodPrice } from '../api/tariffs';
import { formatPrice } from '../utils/format';
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
import { Toggle, LocaleTabs, LocalizedInput } from '../components/admin';
import { BackgroundConfigEditor } from '../components/admin/BackgroundConfigEditor';
import type { AnimationConfig } from '@/components/ui/backgrounds/types';
import { DEFAULT_ANIMATION_CONFIG } from '@/components/ui/backgrounds/types';
import { SortableFeatureItem, type FeatureWithId } from '../components/admin/SortableFeatureItem';
import {
SortableSelectedMethodCard,
@@ -102,6 +105,7 @@ export default function AdminLandingEditor() {
discount: false,
methods: false,
gifts: false,
background: false,
footer: false,
});
@@ -127,6 +131,12 @@ export default function AdminLandingEditor() {
const [footerText, setFooterText] = useState<LocaleDict>({});
const [customCss, setCustomCss] = useState('');
// Background config state
const [backgroundConfig, setBackgroundConfig] = useState<AnimationConfig>({
...DEFAULT_ANIMATION_CONFIG,
enabled: false,
});
// Discount state
const [discountPercent, setDiscountPercent] = useState<number | null>(null);
const [discountOverrides, setDiscountOverrides] = useState<Record<string, number>>({});
@@ -245,6 +255,9 @@ export default function AdminLandingEditor() {
setGiftEnabled(landingData.gift_enabled);
setFooterText(toLocaleDict(landingData.footer_text));
setCustomCss(landingData.custom_css ?? '');
if (landingData.background_config) {
setBackgroundConfig(landingData.background_config);
}
setDiscountPercent(landingData.discount_percent ?? null);
setDiscountOverrides(landingData.discount_overrides ?? {});
setDiscountStartsAt(
@@ -364,6 +377,7 @@ export default function AdminLandingEditor() {
discountPercent !== null && discountEndsAt ? new Date(discountEndsAt).toISOString() : null,
discount_badge_text:
discountPercent !== null ? (nonEmptyDict(discountBadgeText) ?? null) : null,
background_config: backgroundConfig.enabled ? backgroundConfig : null,
};
if (isEdit) {
@@ -1056,6 +1070,15 @@ export default function AdminLandingEditor() {
</div>
</Section>
{/* Background Section */}
<Section
title={t('admin.landings.background', 'Background')}
open={openSections.background}
onToggle={() => toggleSection('background')}
>
<BackgroundConfigEditor value={backgroundConfig} onChange={setBackgroundConfig} />
</Section>
{/* Footer & Custom CSS Section */}
<Section
title={t('admin.landings.content')}

View File

@@ -12,6 +12,7 @@ import type {
LandingPaymentMethod,
PurchaseRequest,
} from '../api/landings';
import { StaticBackgroundRenderer } from '../components/backgrounds/BackgroundRenderer';
import LanguageSwitcher from '../components/LanguageSwitcher';
import { cn } from '../lib/utils';
import { getApiErrorMessage } from '../utils/api-error';
@@ -963,6 +964,7 @@ export default function QuickPurchase() {
return (
<div className="min-h-dvh overflow-x-hidden bg-dark-950">
{config.background_config && <StaticBackgroundRenderer config={config.background_config} />}
<div className="mx-auto max-w-5xl px-4 py-8 sm:px-6 lg:px-8">
{/* Language switcher */}
<div className="mb-4 flex justify-end">