Hintergründe zum EventInvitePage Layout Customizer hinzugefügt. Badge und CTA entfernt, Textfelder zu Textareas gemacht. Geschenkgutscheine verbessert, E-Mail-Versand ergänzt + Resend + Confirmationseite mit Code-Copy und Link zur Package-Seite, die den Code als URL-Parameter enthält.

This commit is contained in:
Codex Agent
2025-12-08 16:20:04 +01:00
parent 046e2fe3ec
commit 4784c23e70
35 changed files with 1503 additions and 136 deletions

View File

@@ -34,6 +34,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import { ensureFontLoaded, useTenantFonts } from '../../lib/fonts';
import { preloadedBackgrounds, type BackgroundImageOption } from './invite-layout/backgrounds';
const DEFAULT_FONT_VALUE = '__default';
@@ -115,6 +116,12 @@ function sanitizePayload(payload: QrLayoutCustomization): QrLayoutCustomization
normalized.background_color = sanitizeColor(payload.background_color ?? null) ?? undefined;
normalized.secondary_color = sanitizeColor(payload.secondary_color ?? null) ?? undefined;
normalized.badge_color = sanitizeColor(payload.badge_color ?? null) ?? undefined;
if (typeof payload.background_image === 'string') {
const trimmed = payload.background_image.trim();
normalized.background_image = trimmed.length ? trimmed : undefined;
} else {
normalized.background_image = undefined;
}
if (payload.background_gradient && typeof payload.background_gradient === 'object') {
const { angle, stops } = payload.background_gradient as { angle?: number; stops?: unknown };
@@ -192,6 +199,7 @@ type InviteLayoutCustomizerPanelProps = {
invite: EventQrInvite | null;
eventName: string;
eventDate: string | null;
backgroundImages?: BackgroundImageOption[];
saving: boolean;
resetting: boolean;
onSave: (customization: QrLayoutCustomization) => Promise<void>;
@@ -217,6 +225,7 @@ export function InviteLayoutCustomizerPanel({
initialCustomization,
draftCustomization,
onDraftChange,
backgroundImages = preloadedBackgrounds,
}: InviteLayoutCustomizerPanelProps): React.JSX.Element {
const { t } = useTranslation('management');
const { fonts: availableFonts, isLoading: fontsLoading } = useTenantFonts();
@@ -792,6 +801,7 @@ export function InviteLayoutCustomizerPanel({
background_color: sanitizeColor((reuseCustomization ? activeCustomization?.background_color : activeLayout.preview?.background) ?? null) ?? '#FFFFFF',
secondary_color: reuseCustomization ? activeCustomization?.secondary_color ?? '#1F2937' : '#1F2937',
badge_color: reuseCustomization ? activeCustomization?.badge_color ?? '#2563EB' : '#2563EB',
background_image: reuseCustomization ? activeCustomization?.background_image ?? null : null,
background_gradient: reuseCustomization ? activeCustomization?.background_gradient ?? activeLayout.preview?.background_gradient ?? null : activeLayout.preview?.background_gradient ?? null,
logo_data_url: reuseCustomization ? activeCustomization?.logo_data_url ?? activeCustomization?.logo_url ?? null : null,
mode: reuseCustomization ? activeCustomization?.mode : 'standard',
@@ -1285,9 +1295,10 @@ export function InviteLayoutCustomizerPanel({
blocks.push(
<div className="space-y-2" key={`${element.id}-binding`}>
<Label>{binding.label}</Label>
<Input
<Textarea
value={value}
onChange={(event) => updateForm(binding.field, event.target.value as never)}
className="min-h-[72px]"
/>
</div>
);
@@ -1551,6 +1562,7 @@ export function InviteLayoutCustomizerPanel({
logoDataUrl: form.logo_data_url ?? form.logo_url ?? null,
backgroundColor: form.background_color ?? activeLayout?.preview?.background ?? '#FFFFFF',
backgroundGradient: form.background_gradient ?? activeLayout?.preview?.background_gradient ?? null,
backgroundImageUrl: form.background_image ?? null,
readOnly: true,
selectedId: null,
} as const;
@@ -1595,6 +1607,7 @@ export function InviteLayoutCustomizerPanel({
logoDataUrl: form.logo_data_url ?? form.logo_url ?? null,
backgroundColor: form.background_color ?? activeLayout?.preview?.background ?? '#FFFFFF',
backgroundGradient: form.background_gradient ?? activeLayout?.preview?.background_gradient ?? null,
backgroundImageUrl: form.background_image ?? null,
readOnly: true,
selectedId: null,
} as const;
@@ -1738,18 +1751,20 @@ export function InviteLayoutCustomizerPanel({
<div className="grid gap-4">
<div className="space-y-2">
<Label htmlFor="invite-headline">{t('invites.customizer.fields.headline', 'Überschrift')}</Label>
<Input
<Textarea
id="invite-headline"
value={form.headline ?? ''}
onChange={(event) => updateForm('headline', event.target.value)}
className="min-h-[68px]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="invite-subtitle">{t('invites.customizer.fields.subtitle', 'Unterzeile')}</Label>
<Input
<Textarea
id="invite-subtitle"
value={form.subtitle ?? ''}
onChange={(event) => updateForm('subtitle', event.target.value)}
className="min-h-[68px]"
/>
</div>
<div className="space-y-2">
@@ -1761,39 +1776,23 @@ export function InviteLayoutCustomizerPanel({
className="min-h-[96px]"
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="invite-badge">{t('invites.customizer.fields.badge', 'Badge-Label')}</Label>
<Input
id="invite-badge"
value={form.badge_label ?? ''}
onChange={(event) => updateForm('badge_label', event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="invite-cta">{t('invites.customizer.fields.cta', 'Call-to-Action')}</Label>
<Input
id="invite-cta"
value={form.cta_label ?? ''}
onChange={(event) => updateForm('cta_label', event.target.value)}
/>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="invite-link-heading">{t('invites.customizer.fields.linkHeading', 'Link-Überschrift')}</Label>
<Input
<Textarea
id="invite-link-heading"
value={form.link_heading ?? ''}
onChange={(event) => updateForm('link_heading', event.target.value)}
className="min-h-[68px]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="invite-link-label">{t('invites.customizer.fields.linkLabel', 'Link/Begleittext')}</Label>
<Input
<Textarea
id="invite-link-label"
value={form.link_label ?? ''}
onChange={(event) => updateForm('link_label', event.target.value)}
className="min-h-[68px]"
/>
</div>
</div>
@@ -1832,18 +1831,56 @@ export function InviteLayoutCustomizerPanel({
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="invite-badge-color">{t('invites.customizer.fields.badgeColor', 'Badge')}</Label>
<Input
id="invite-badge-color"
type="color"
value={form.badge_color ?? '#2563EB'}
onChange={(event) => updateForm('badge_color', event.target.value)}
className="h-11"
/>
</div>
</div>
{backgroundImages.length ? (
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<Label>{t('invites.customizer.fields.backgroundImage', 'Hintergrundbild')}</Label>
{form.background_image ? (
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => {
updateForm('background_image', null as never);
updateForm('background_gradient', null as never);
}}
>
{t('invites.customizer.actions.removeBackgroundImage', 'Bild entfernen')}
</Button>
) : null}
</div>
<p className="text-xs text-muted-foreground">
{t('invites.customizer.fields.backgroundImageHint', 'Wähle ein Bild. Es ersetzt den Farbverlauf und füllt den ganzen Hintergrund.')}
</p>
<div className="grid gap-3 sm:grid-cols-3">
{backgroundImages.map((item) => {
const isActive = form.background_image === item.url;
return (
<button
key={item.id}
type="button"
onClick={() => {
updateForm('background_image', item.url as never);
updateForm('background_gradient', null as never);
}}
className={cn(
'group overflow-hidden rounded-lg border text-left shadow-sm transition focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
isActive ? 'border-primary ring-2 ring-primary/50' : 'border-[var(--tenant-border-strong)]'
)}
>
<div className="aspect-[3/4] w-full overflow-hidden bg-[var(--tenant-surface-muted)]">
<img src={item.url} alt={item.label} className="h-full w-full object-cover transition group-hover:scale-105" />
</div>
<div className="p-2 text-xs text-muted-foreground line-clamp-1">{item.label}</div>
</button>
);
})}
</div>
</div>
) : null}
<div className="space-y-2">
<Label>{t('invites.customizer.fields.logo', 'Logo')}</Label>
{form.logo_data_url ? (
@@ -2075,6 +2112,7 @@ export function InviteLayoutCustomizerPanel({
onChange={updateElement}
background={form.background_color ?? activeLayout.preview?.background ?? '#FFFFFF'}
gradient={form.background_gradient ?? activeLayout.preview?.background_gradient ?? null}
backgroundImageUrl={form.background_image ?? null}
accent={form.accent_color ?? activeLayout.preview?.accent ?? '#6366F1'}
text={form.text_color ?? activeLayout.preview?.text ?? '#111827'}
secondary={form.secondary_color ?? '#1F2937'}