import React from 'react'; import { useTranslation } from 'react-i18next'; import { Download, Loader2, Plus, Printer, RotateCcw, Save, UploadCloud } from 'lucide-react'; import { Rnd } from 'react-rnd'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { Textarea } from '@/components/ui/textarea'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { cn } from '@/lib/utils'; import type { EventQrInvite, EventQrInviteLayout } from '../../api'; import { authorizedFetch, isAuthError } from '../../auth/tokens'; export type QrLayoutCustomization = { layout_id?: string; headline?: string; subtitle?: string; description?: string; badge_label?: string; instructions_heading?: string; instructions?: string[]; link_heading?: string; link_label?: string; cta_label?: string; accent_color?: string; text_color?: string; background_color?: string; secondary_color?: string; badge_color?: string; background_gradient?: { angle?: number; stops?: string[] } | null; logo_data_url?: string | null; logo_url?: string | null; mode?: 'standard' | 'advanced'; elements?: AdvancedLayoutElementPayload[]; }; type AdvancedLayoutElement = { id: string; type: 'qr' | 'headline' | 'subtitle' | 'description' | 'link' | 'badge' | 'logo'; x: number; y: number; width: number; height: number; fontSize?: number; align?: 'left' | 'center' | 'right'; content?: string | null; }; type AdvancedLayoutElementPayload = { id: string; type: AdvancedLayoutElement['type']; x: number; y: number; width: number; height: number; font_size?: number; align?: 'left' | 'center' | 'right'; content?: string | null; }; type AdvancedSerializationContext = { form: QrLayoutCustomization; eventName: string; inviteUrl: string; instructions: string[]; qrSize: number; badgeFallback: string; logoUrl: string | null; }; const ADVANCED_CANVAS_WIDTH = 1080; const ADVANCED_CANVAS_HEIGHT = 1520; const ADVANCED_MIN_QR = 240; const ADVANCED_MAX_QR = 720; const ADVANCED_MIN_TEXT_WIDTH = 160; const ADVANCED_MIN_TEXT_HEIGHT = 80; function clamp(value: number, min: number, max: number): number { if (Number.isNaN(value)) { return min; } return Math.min(Math.max(value, min), max); } function buildDefaultAdvancedElements( layout: EventQrInviteLayout | null, form: QrLayoutCustomization, eventName: string, qrSize: number ): AdvancedLayoutElement[] { const resolvedQrSize = Math.min(Math.max(qrSize, ADVANCED_MIN_QR), ADVANCED_MAX_QR); return [ { id: 'headline', type: 'headline', x: 80, y: 120, width: 520, height: 160, fontSize: 60, content: form.headline ?? eventName, }, { id: 'description', type: 'description', x: 80, y: 320, width: 520, height: 240, fontSize: 28, content: form.description ?? layout?.description ?? '', }, { id: 'qr', type: 'qr', x: 640, y: 320, width: resolvedQrSize, height: resolvedQrSize, }, { id: 'link', type: 'link', x: 640, y: 320 + resolvedQrSize + 40, width: 360, height: 140, fontSize: 26, content: form.link_label ?? '', }, { id: 'badge', type: 'badge', x: 80, y: 60, width: 260, height: 70, fontSize: 28, content: form.badge_label ?? 'Digitale Gästebox', }, ]; } function normalizeAdvancedElements(elements: AdvancedLayoutElement[]): AdvancedLayoutElement[] { return (elements ?? []).map((element) => ({ ...element, x: Number(element.x ?? 0), y: Number(element.y ?? 0), width: Number(element.width ?? 0), height: Number(element.height ?? 0), fontSize: element.fontSize ? Number(element.fontSize) : undefined, })); } function convertPayloadToElements(payload: AdvancedLayoutElementPayload[] | undefined | null): AdvancedLayoutElement[] { if (!Array.isArray(payload)) { return []; } return payload.map((item) => ({ id: item.id, type: item.type, x: Number(item.x ?? 0), y: Number(item.y ?? 0), width: Number(item.width ?? 0), height: Number(item.height ?? 0), fontSize: item.font_size ? Number(item.font_size) : undefined, align: item.align ?? 'left', content: item.content ?? null, })); } function clampElementToCanvas(element: AdvancedLayoutElement): AdvancedLayoutElement { const minWidth = element.type === 'qr' ? ADVANCED_MIN_QR : ADVANCED_MIN_TEXT_WIDTH; const minHeight = element.type === 'qr' ? ADVANCED_MIN_QR : ADVANCED_MIN_TEXT_HEIGHT; const width = clamp(element.width, minWidth, ADVANCED_CANVAS_WIDTH); const height = clamp(element.height, minHeight, ADVANCED_CANVAS_HEIGHT); const maxX = Math.max(ADVANCED_CANVAS_WIDTH - width, 0); const maxY = Math.max(ADVANCED_CANVAS_HEIGHT - height, 0); return { ...element, width, height, x: clamp(element.x, 0, maxX), y: clamp(element.y, 0, maxY), }; } function serializeAdvancedElements( elements: AdvancedLayoutElement[], context: AdvancedSerializationContext ): AdvancedLayoutElementPayload[] { return normalizeAdvancedElements(elements).map((element) => { const base = clampElementToCanvas(element); let content: string | null = base.content ?? null; switch (base.type) { case 'headline': content = context.form.headline ?? context.eventName; break; case 'subtitle': content = context.form.subtitle ?? ''; break; case 'description': content = context.form.description ?? ''; break; case 'link': content = context.form.link_label ?? context.inviteUrl; break; case 'badge': content = context.form.badge_label ?? context.badgeFallback; break; case 'logo': content = context.logoUrl ?? context.form.logo_url ?? null; break; default: break; } return { id: base.id, type: base.type, x: Math.round(base.x), y: Math.round(base.y), width: Math.round(base.width), height: Math.round(base.height), font_size: base.fontSize ? Math.round(base.fontSize) : undefined, align: base.align, content, }; }); } type InviteLayoutCustomizerPanelProps = { invite: EventQrInvite | null; eventName: string; saving: boolean; resetting: boolean; onSave: (customization: QrLayoutCustomization) => Promise; onReset: () => Promise; initialCustomization: QrLayoutCustomization | null; mode: 'standard' | 'advanced'; }; const MAX_INSTRUCTIONS = 5; export function InviteLayoutCustomizerPanel({ invite, eventName, saving, resetting, onSave, onReset, initialCustomization, mode, }: InviteLayoutCustomizerPanelProps): React.JSX.Element { const { t } = useTranslation('management'); const inviteUrl = invite?.url ?? ''; const qrCodeDataUrl = invite?.qr_code_data_url ?? null; const defaultInstructions = React.useMemo(() => { const value = t('tasks.customizer.defaults.instructions', { returnObjects: true }) as unknown; return Array.isArray(value) ? (value as string[]) : ['QR-Code scannen', 'Profil anlegen', 'Fotos teilen']; }, [t]); const [availableLayouts, setAvailableLayouts] = React.useState(invite?.layouts ?? []); const [layoutsLoading, setLayoutsLoading] = React.useState(false); const [layoutsError, setLayoutsError] = React.useState(null); const [selectedLayoutId, setSelectedLayoutId] = React.useState(initialCustomization?.layout_id ?? invite?.layouts?.[0]?.id); const [form, setForm] = React.useState({}); const [instructions, setInstructions] = React.useState([]); const [error, setError] = React.useState(null); const formRef = React.useRef(null); const [downloadBusy, setDownloadBusy] = React.useState(null); const [printBusy, setPrintBusy] = React.useState(false); const [activeLayoutIndex, setActiveLayoutIndex] = React.useState(() => { if (!availableLayouts.length) { return 0; } const initialIndex = availableLayouts.findIndex((layout) => layout.id === selectedLayoutId); return initialIndex >= 0 ? initialIndex : 0; }); const isAdvanced = mode === 'advanced'; const [elements, setElements] = React.useState([]); const [activeElementId, setActiveElementId] = React.useState(null); const [canvasScale, setCanvasScale] = React.useState(0.52); const [mobilePreviewOpen, setMobilePreviewOpen] = React.useState(false); const [showFloatingActions, setShowFloatingActions] = React.useState(false); const actionsSentinelRef = React.useRef(null); const activeLayout = React.useMemo(() => { if (!availableLayouts.length) { return null; } if (selectedLayoutId) { const match = availableLayouts.find((layout) => layout.id === selectedLayoutId); if (match) { return match; } } return availableLayouts[activeLayoutIndex] ?? availableLayouts[0]; }, [availableLayouts, selectedLayoutId, activeLayoutIndex]); const activeLayoutQrSize = React.useMemo(() => { if (initialCustomization?.mode === 'advanced' && Array.isArray(initialCustomization.elements)) { const qrElement = initialCustomization.elements.find((element) => element?.type === 'qr'); if (qrElement && typeof qrElement.width === 'number' && qrElement.width > 0) { return qrElement.width; } } return activeLayout?.preview?.qr_size_px ?? 500; }, [initialCustomization?.mode, initialCustomization?.elements, activeLayout?.preview?.qr_size_px]); const updateElement = React.useCallback( ( id: string, updater: Partial | ((element: AdvancedLayoutElement) => Partial) ) => { setElements((current) => current.map((element) => { if (element.id !== id) { return element; } const patch = typeof updater === 'function' ? updater(element) : updater; return clampElementToCanvas({ ...element, ...patch }); }) ); }, [] ); const handleResetAdvanced = React.useCallback(() => { if (!activeLayout) { setElements([]); return; } setElements(buildDefaultAdvancedElements(activeLayout, form, eventName, activeLayoutQrSize)); setActiveElementId(null); }, [activeLayout, form, eventName, activeLayoutQrSize]); React.useEffect(() => { if (!invite) { setAvailableLayouts([]); setSelectedLayoutId(undefined); return; } const layouts = invite.layouts ?? []; setAvailableLayouts(layouts); setLayoutsError(null); setSelectedLayoutId((current) => { if (current && layouts.some((layout) => layout.id === current)) { return current; } if (initialCustomization?.layout_id && layouts.some((layout) => layout.id === initialCustomization.layout_id)) { return initialCustomization.layout_id; } return layouts[0]?.id; }); }, [invite?.id, initialCustomization?.layout_id]); React.useEffect(() => { let cancelled = false; async function loadLayouts(url: string) { try { setLayoutsLoading(true); setLayoutsError(null); const target = (() => { try { if (url.startsWith('http://') || url.startsWith('https://')) { const parsed = new URL(url); return parsed.pathname + parsed.search; } } catch (parseError) { console.warn('[Invites] Failed to parse layout URL', parseError); } return url; })(); const response = await authorizedFetch(target, { method: 'GET', headers: { Accept: 'application/json' }, }); if (!response.ok) { console.error('[Invites] Layout request failed', response.status, response.statusText); throw new Error(`Failed with status ${response.status}`); } const json = await response.json(); const items = Array.isArray(json?.data) ? (json.data as EventQrInviteLayout[]) : []; if (!cancelled) { setAvailableLayouts(items); setSelectedLayoutId((current) => { if (current && items.some((layout) => layout.id === current)) { return current; } if (initialCustomization?.layout_id && items.some((layout) => layout.id === initialCustomization.layout_id)) { return initialCustomization.layout_id; } return items[0]?.id; }); } } catch (err) { if (!cancelled) { console.error('[Invites] Failed to load layouts', err); setLayoutsError(t('invites.customizer.loadingError', 'Layouts konnten nicht geladen werden.')); } } finally { if (!cancelled) { setLayoutsLoading(false); } } } if (!invite || availableLayouts.length > 0 || !invite.layouts_url) { return () => { cancelled = true; }; } void loadLayouts(invite.layouts_url); return () => { cancelled = true; }; }, [invite, availableLayouts.length, initialCustomization?.layout_id, t]); React.useEffect(() => { if (!availableLayouts.length) { return; } setSelectedLayoutId((current) => { if (current && availableLayouts.some((layout) => layout.id === current)) { return current; } if (initialCustomization?.layout_id && availableLayouts.some((layout) => layout.id === initialCustomization.layout_id)) { return initialCustomization.layout_id; } return availableLayouts[0].id; }); }, [availableLayouts, initialCustomization?.layout_id]); React.useEffect(() => { if (!invite || !activeLayout) { setForm({}); setInstructions([]); return; } const baseInstructions = Array.isArray(initialCustomization?.instructions) && initialCustomization.instructions?.length ? [...(initialCustomization.instructions as string[])] : [...defaultInstructions]; setInstructions(baseInstructions); setForm({ layout_id: activeLayout.id, headline: initialCustomization?.headline ?? eventName, subtitle: initialCustomization?.subtitle ?? activeLayout.subtitle ?? '', description: initialCustomization?.description ?? activeLayout.description ?? '', badge_label: initialCustomization?.badge_label ?? t('tasks.customizer.defaults.badgeLabel'), instructions_heading: initialCustomization?.instructions_heading ?? t('tasks.customizer.defaults.instructionsHeading'), link_heading: initialCustomization?.link_heading ?? t('tasks.customizer.defaults.linkHeading'), link_label: initialCustomization?.link_label ?? inviteUrl, cta_label: initialCustomization?.cta_label ?? t('tasks.customizer.defaults.ctaLabel'), accent_color: initialCustomization?.accent_color ?? activeLayout.preview?.accent ?? '#6366F1', text_color: initialCustomization?.text_color ?? activeLayout.preview?.text ?? '#111827', background_color: initialCustomization?.background_color ?? activeLayout.preview?.background ?? '#FFFFFF', secondary_color: initialCustomization?.secondary_color ?? 'rgba(15,23,42,0.08)', badge_color: initialCustomization?.badge_color ?? activeLayout.preview?.accent ?? '#2563EB', background_gradient: initialCustomization?.background_gradient ?? activeLayout.preview?.background_gradient ?? null, logo_data_url: initialCustomization?.logo_data_url ?? initialCustomization?.logo_url ?? null, }); setError(null); }, [invite?.id, activeLayout?.id, defaultInstructions, initialCustomization, eventName, inviteUrl, t]); React.useEffect(() => { if (!isAdvanced) { setActiveElementId(null); return; } if (!activeLayout) { setElements([]); return; } if (initialCustomization?.mode === 'advanced' && Array.isArray(initialCustomization.elements) && initialCustomization.elements.length) { setElements(normalizeAdvancedElements(convertPayloadToElements(initialCustomization.elements))); return; } setElements(buildDefaultAdvancedElements(activeLayout, form, eventName, activeLayoutQrSize)); }, [isAdvanced, activeLayout?.id, invite?.id, activeLayoutQrSize]); React.useEffect(() => { if (typeof IntersectionObserver === 'undefined') { setShowFloatingActions(false); return; } const node = actionsSentinelRef.current; if (!node) { setShowFloatingActions(false); return; } const observer = new IntersectionObserver(([entry]) => { setShowFloatingActions(!entry.isIntersecting); }); observer.observe(node); return () => { observer.disconnect(); }; }, [invite?.id, activeLayout?.id]); const effectiveInstructions = instructions.filter((entry) => entry.trim().length > 0); const advancedPreview = React.useMemo(() => ({ headline: form.headline ?? eventName, subtitle: form.subtitle ?? '', description: form.description ?? activeLayout?.description ?? '', link: form.link_label ?? inviteUrl, badge: form.badge_label ?? t('tasks.customizer.defaults.badgeLabel'), background: form.background_color ?? activeLayout?.preview?.background ?? '#FFFFFF', text: form.text_color ?? activeLayout?.preview?.text ?? '#111827', accent: form.accent_color ?? activeLayout?.preview?.accent ?? '#6366F1', logo: form.logo_data_url ?? form.logo_url ?? null, instructions: effectiveInstructions, }), [form.headline, form.subtitle, form.description, form.link_label, form.badge_label, form.background_color, form.text_color, form.accent_color, form.logo_data_url, form.logo_url, eventName, activeLayout?.description, activeLayout?.preview?.background, activeLayout?.preview?.text, activeLayout?.preview?.accent, effectiveInstructions, inviteUrl, t]); const renderActionButtons = (mode: 'inline' | 'floating') => ( <> ); const previewStyles = React.useMemo(() => { const gradient = form.background_gradient ?? activeLayout?.preview?.background_gradient ?? null; if (gradient?.stops && gradient.stops.length > 0) { const angle = gradient.angle ?? 180; const stops = gradient.stops.join(', '); return { backgroundImage: `linear-gradient(${angle}deg, ${stops})` }; } return { backgroundColor: form.background_color ?? activeLayout?.preview?.background ?? '#F8FAFC' }; }, [form.background_color, form.background_gradient, activeLayout]); const previewStack = (

{t('invites.customizer.preview.title', 'Live-Vorschau')}

{t('invites.customizer.preview.subtitle', 'So sieht dein Layout beim Export aus.')}

{activeLayout?.formats?.map((format) => { const key = String(format ?? '').toLowerCase(); const url = activeLayout.download_urls?.[key]; if (!url) return null; return ( ); })}
{form.badge_label || t('tasks.customizer.defaults.badgeLabel')}

{form.headline || eventName}

{form.subtitle ? (

{form.subtitle}

) : null}
{form.description ? (

{form.description}

) : null}
{form.instructions_heading}
    {effectiveInstructions.slice(0, 4).map((item, index) => (
  1. {index + 1}. {item}
  2. ))}
{form.link_heading} {t('invites.customizer.preview.readyForGuests', 'Bereit für Gäste')}
{qrCodeDataUrl ? ( {t('invites.customizer.preview.qrAlt', ) : (
{t('invites.customizer.preview.qrPlaceholder', 'QR-Code folgt nach dem Speichern')}
)}
{form.link_label || inviteUrl}

{t('invites.customizer.preview.instructions', 'Dieser Link führt Gäste direkt zur Galerie und funktioniert zusammen mit dem QR-Code auf dem Ausdruck.')}

{form.logo_data_url ? (
Logo preview
) : null}
); function updateForm(key: T, value: QrLayoutCustomization[T]) { setForm((prev) => ({ ...prev, [key]: value })); } function handleLayoutSelect(layout: EventQrInviteLayout) { setSelectedLayoutId(layout.id); updateForm('layout_id', layout.id); setForm((prev) => ({ ...prev, accent_color: prev.accent_color ?? layout.preview?.accent ?? '#6366F1', text_color: prev.text_color ?? layout.preview?.text ?? '#111827', background_color: prev.background_color ?? layout.preview?.background ?? '#FFFFFF', background_gradient: prev.background_gradient ?? layout.preview?.background_gradient ?? null, })); if (isAdvanced) { setElements(buildDefaultAdvancedElements(layout, form, eventName, layout.preview?.qr_size_px ?? activeLayoutQrSize)); setActiveElementId(null); } } React.useEffect(() => { if (!availableLayouts.length) { return; } const index = availableLayouts.findIndex((layout) => layout.id === selectedLayoutId); if (index >= 0 && index !== activeLayoutIndex) { setActiveLayoutIndex(index); } }, [availableLayouts, selectedLayoutId, activeLayoutIndex]); function rotateLayout(delta: number) { if (!availableLayouts.length) { return; } const nextIndex = (activeLayoutIndex + delta + availableLayouts.length) % availableLayouts.length; setActiveLayoutIndex(nextIndex); handleLayoutSelect(availableLayouts[nextIndex]!); } function selectLayoutAt(index: number) { if (index < 0 || index >= availableLayouts.length) { return; } setActiveLayoutIndex(index); handleLayoutSelect(availableLayouts[index]!); } function handleInstructionChange(index: number, value: string) { setInstructions((prev) => { const next = [...prev]; next[index] = value; return next; }); } function handleAddInstruction() { setInstructions((prev) => (prev.length < MAX_INSTRUCTIONS ? [...prev, ''] : prev)); } function handleRemoveInstruction(index: number) { setInstructions((prev) => prev.filter((_, idx) => idx !== index)); } function handleLogoUpload(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) { return; } if (file.size > 1024 * 1024) { setError(t('tasks.customizer.errors.logoTooLarge', 'Das Logo darf maximal 1 MB groß sein.')); return; } const reader = new FileReader(); reader.onload = () => { updateForm('logo_data_url', typeof reader.result === 'string' ? reader.result : null); setError(null); }; reader.readAsDataURL(file); } function handleLogoRemove() { updateForm('logo_data_url', null); } async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (!invite || !activeLayout) { return; } const payload: QrLayoutCustomization = { ...form, layout_id: activeLayout.id, instructions: effectiveInstructions, }; if (isAdvanced) { const serializationContext: AdvancedSerializationContext = { form, eventName, inviteUrl, instructions: effectiveInstructions, qrSize: activeLayoutQrSize, badgeFallback: t('tasks.customizer.defaults.badgeLabel'), logoUrl: form.logo_url ?? null, }; payload.mode = 'advanced'; payload.elements = serializeAdvancedElements(elements.length ? elements : buildDefaultAdvancedElements(activeLayout, form, eventName, activeLayoutQrSize), serializationContext); } else { payload.mode = 'standard'; payload.elements = undefined; } await onSave(payload); } async function handleResetClick() { await onReset(); } function resolveInternalUrl(rawUrl: string): string { try { const parsed = new URL(rawUrl, window.location.origin); if (parsed.origin === window.location.origin) { return parsed.pathname + parsed.search; } } catch (resolveError) { console.warn('[Invites] Unable to resolve download url', resolveError); } return rawUrl; } async function handleDownload(format: string, rawUrl: string): Promise { if (!rawUrl || !invite) { return; } const normalizedFormat = format.toLowerCase(); const filenameStem = invite.token || 'invite'; setDownloadBusy(normalizedFormat); setError(null); try { const response = await authorizedFetch(resolveInternalUrl(rawUrl), { headers: { Accept: normalizedFormat === 'pdf' ? 'application/pdf' : 'image/svg+xml', }, }); if (!response.ok) { throw new Error(`Unexpected status ${response.status}`); } const blob = await response.blob(); const objectUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = objectUrl; link.download = `${filenameStem}-${normalizedFormat}.${normalizedFormat}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(objectUrl), 1000); } catch (downloadError) { console.error('[Invites] Download failed', downloadError); const message = isAuthError(downloadError) ? t('invites.customizer.errors.auth', 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.') : t('invites.customizer.errors.downloadFailed', 'Download fehlgeschlagen. Bitte versuche es erneut.'); setError(message); } finally { setDownloadBusy(null); } } async function handlePrint(preferredUrl?: string | null): Promise { const rawUrl = preferredUrl ?? activeLayout?.download_urls?.pdf ?? activeLayout?.download_urls?.a4 ?? null; if (!rawUrl) { setError(t('invites.labels.noPrintSource', 'Keine druckbare Version verfügbar.')); return; } setPrintBusy(true); setError(null); try { const response = await authorizedFetch(resolveInternalUrl(rawUrl), { headers: { Accept: 'application/pdf' }, }); if (!response.ok) { throw new Error(`Unexpected status ${response.status}`); } const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const printWindow = window.open(blobUrl, '_blank', 'noopener,noreferrer'); if (!printWindow) { throw new Error('window-blocked'); } printWindow.onload = () => { try { printWindow.focus(); printWindow.print(); } catch (printError) { console.error('[Invites] Browser print failed', printError); } }; setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000); } catch (printError) { console.error('[Invites] Print failed', printError); const message = isAuthError(printError) ? t('invites.customizer.errors.auth', 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.') : t('invites.customizer.errors.printFailed', 'Druck konnte nicht gestartet werden.'); setError(message); } finally { setPrintBusy(false); } } if (!invite) { return ( ); } if (!availableLayouts.length) { if (layoutsLoading) { return ( ); } return ( ); } if (!activeLayout) { return ( ); } return (

{t('invites.customizer.heading', 'Layout anpassen')}

{t('invites.customizer.copy', 'Verleihe der Einladung euren Ton: Texte, Farben und Logo lassen sich live bearbeiten.')}

{error ? (
{error}
) : null} {invite ? (
{t('invites.customizer.preview.mobileTitle', 'Einladungsvorschau')}
{previewStack}
) : null}

{t('invites.customizer.sections.layouts', 'Layouts')}

{t('invites.customizer.sections.layoutsHint', 'Wähle eine Vorlage als Basis aus. Du kannst jederzeit wechseln.')}

{availableLayouts.length > 1 ? t('invites.customizer.carousel.hint', { defaultValue: 'Nutze die Pfeile oder Pfeiltasten, um weitere Layouts zu entdecken.', }) : null}

{activeLayout.name}

{activeLayout.subtitle ?

{activeLayout.subtitle}

: null} {activeLayout.description ?

{activeLayout.description}

: null}
{t('invites.customizer.sections.text', 'Texte')} {t('invites.customizer.sections.instructions', 'Schritt-für-Schritt')} {t('invites.customizer.sections.branding', 'Farbgebung')}
updateForm('headline', event.target.value)} />
updateForm('subtitle', event.target.value)} />