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:
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user