layouts schick gemacht und packagelimits weiter implementiert

This commit is contained in:
Codex Agent
2025-11-01 22:55:13 +01:00
parent 79b209de9a
commit 8e6c66f0db
16 changed files with 756 additions and 422 deletions

View File

@@ -200,6 +200,9 @@ export function InviteLayoutCustomizerPanel({
const inviteUrl = invite?.url ?? '';
const qrCodeDataUrl = invite?.qr_code_data_url ?? null;
if (!qrCodeDataUrl) {
console.warn('QR DataURL is null - using fallback in canvas');
}
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'];
@@ -220,6 +223,7 @@ export function InviteLayoutCustomizerPanel({
const [showFloatingActions, setShowFloatingActions] = React.useState(false);
const [zoomScale, setZoomScale] = React.useState(1);
const [fitScale, setFitScale] = React.useState(1);
const [previewMode, setPreviewMode] = React.useState<'fit' | 'full'>('fit');
const fitScaleRef = React.useRef(1);
const manualZoomRef = React.useRef(false);
const actionsSentinelRef = React.useRef<HTMLDivElement | null>(null);
@@ -262,7 +266,9 @@ export function InviteLayoutCustomizerPanel({
const widthScale = availableWidth / CANVAS_WIDTH;
const heightScale = availableHeight / CANVAS_HEIGHT;
const nextRaw = Math.min(widthScale, heightScale);
const baseScale = Number.isFinite(nextRaw) && nextRaw > 0 ? Math.min(nextRaw, 1) : 1;
let baseScale = Number.isFinite(nextRaw) && nextRaw > 0 ? nextRaw : 1;
const minScale = 0.3;
baseScale = Math.max(baseScale, minScale);
const clamped = clampZoom(baseScale);
fitScaleRef.current = clamped;
@@ -462,10 +468,12 @@ export function InviteLayoutCustomizerPanel({
return activeLayout?.preview?.qr_size_px ?? 500;
}, [elements, initialCustomization?.mode, initialCustomization?.elements, activeLayout?.preview?.qr_size_px]);
const effectiveScale = React.useMemo(
() => clampZoom(Number.isFinite(zoomScale) && zoomScale > 0 ? zoomScale : fitScale),
[clampZoom, zoomScale, fitScale],
);
const effectiveScale = React.useMemo(() => {
if (previewMode === 'full') {
return 1.0;
}
return clampZoom(Number.isFinite(zoomScale) && zoomScale > 0 ? zoomScale : fitScale);
}, [clampZoom, zoomScale, fitScale, previewMode]);
const zoomPercent = Math.round(effectiveScale * 100);
const updateElement = React.useCallback(
@@ -640,8 +648,8 @@ export function InviteLayoutCustomizerPanel({
accent_color: sanitizeColor((reuseCustomization ? initialCustomization?.accent_color : activeLayout.preview?.accent) ?? null) ?? '#6366F1',
text_color: sanitizeColor((reuseCustomization ? initialCustomization?.text_color : activeLayout.preview?.text) ?? null) ?? '#111827',
background_color: sanitizeColor((reuseCustomization ? initialCustomization?.background_color : activeLayout.preview?.background) ?? null) ?? '#FFFFFF',
secondary_color: sanitizeColor((reuseCustomization ? initialCustomization?.secondary_color : activeLayout.preview?.secondary) ?? null) ?? '#1F2937',
badge_color: sanitizeColor((reuseCustomization ? initialCustomization?.badge_color : activeLayout.preview?.badge ?? activeLayout.preview?.accent) ?? null) ?? '#2563EB',
secondary_color: reuseCustomization ? initialCustomization?.secondary_color ?? '#1F2937' : '#1F2937',
badge_color: reuseCustomization ? initialCustomization?.badge_color ?? '#2563EB' : '#2563EB',
background_gradient: reuseCustomization ? initialCustomization?.background_gradient ?? activeLayout.preview?.background_gradient ?? null : activeLayout.preview?.background_gradient ?? null,
logo_data_url: reuseCustomization ? initialCustomization?.logo_data_url ?? initialCustomization?.logo_url ?? null : null,
});
@@ -1088,7 +1096,7 @@ export function InviteLayoutCustomizerPanel({
updateElement(
elementId,
{
content: typeof nextValue === 'string' ? nextValue : nextValue ?? null,
content: (typeof nextValue === 'string' ? nextValue : String(nextValue ?? '')) as string,
},
{ silent: true }
);
@@ -1252,8 +1260,8 @@ export function InviteLayoutCustomizerPanel({
accent_color: sanitizeColor(layout.preview?.accent ?? prev.accent_color ?? null) ?? '#6366F1',
text_color: sanitizeColor(layout.preview?.text ?? prev.text_color ?? null) ?? '#111827',
background_color: sanitizeColor(layout.preview?.background ?? prev.background_color ?? null) ?? '#FFFFFF',
secondary_color: sanitizeColor(layout.preview?.secondary ?? prev.secondary_color ?? null) ?? '#1F2937',
badge_color: sanitizeColor(layout.preview?.badge ?? prev.badge_color ?? layout.preview?.accent ?? null) ?? '#2563EB',
secondary_color: '#1F2937',
badge_color: '#2563EB',
background_gradient: layout.preview?.background_gradient ?? null,
}));
setInstructions((layout.instructions ?? []).length ? [...(layout.instructions as string[])] : [...defaultInstructions]);
@@ -1351,7 +1359,7 @@ export function InviteLayoutCustomizerPanel({
elements: canvasElements,
accentColor: form.accent_color ?? activeLayout?.preview?.accent ?? '#6366F1',
textColor: form.text_color ?? activeLayout?.preview?.text ?? '#111827',
secondaryColor: form.secondary_color ?? activeLayout?.preview?.secondary ?? '#1F2937',
secondaryColor: form.secondary_color ?? '#1F2937',
badgeColor: form.badge_color ?? form.accent_color ?? '#2563EB',
qrCodeDataUrl,
logoDataUrl: form.logo_data_url ?? form.logo_url ?? null,
@@ -1367,10 +1375,10 @@ export function InviteLayoutCustomizerPanel({
} else if (normalizedFormat === 'pdf') {
const pdfBytes = await generatePdfBytes(
exportOptions,
activeLayout?.paper ?? 'a4',
activeLayout?.orientation ?? 'portrait',
'a4',
'portrait',
);
triggerDownloadFromBlob(new Blob([pdfBytes], { type: 'application/pdf' }), `${filenameStem}.pdf`);
triggerDownloadFromBlob(new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' }), `${filenameStem}.pdf`);
} else {
throw new Error(`Unsupported format: ${normalizedFormat}`);
}
@@ -1395,7 +1403,7 @@ export function InviteLayoutCustomizerPanel({
elements: canvasElements,
accentColor: form.accent_color ?? activeLayout?.preview?.accent ?? '#6366F1',
textColor: form.text_color ?? activeLayout?.preview?.text ?? '#111827',
secondaryColor: form.secondary_color ?? activeLayout?.preview?.secondary ?? '#1F2937',
secondaryColor: form.secondary_color ?? '#1F2937',
badgeColor: form.badge_color ?? form.accent_color ?? '#2563EB',
qrCodeDataUrl,
logoDataUrl: form.logo_data_url ?? form.logo_url ?? null,
@@ -1407,8 +1415,8 @@ export function InviteLayoutCustomizerPanel({
const pdfBytes = await generatePdfBytes(
exportOptions,
activeLayout?.paper ?? 'a4',
activeLayout?.orientation ?? 'portrait',
'a4',
'portrait',
);
await openPdfInNewTab(pdfBytes);
@@ -1815,10 +1823,18 @@ export function InviteLayoutCustomizerPanel({
setZoomScale(clampZoom(Number(event.target.value)));
}}
className="h-1 w-36 overflow-hidden rounded-full"
disabled={false}
disabled={previewMode === 'full'}
aria-label={t('invites.customizer.controls.zoom', 'Zoom')}
/>
<span className="tabular-nums text-sm text-muted-foreground">{zoomPercent}%</span>
<ToggleGroup type="single" value={previewMode} onValueChange={(val) => setPreviewMode(val as 'fit' | 'full')} className="flex">
<ToggleGroupItem value="fit" className="px-2 text-xs">
Fit
</ToggleGroupItem>
<ToggleGroupItem value="full" className="px-2 text-xs">
100%
</ToggleGroupItem>
</ToggleGroup>
<Button
type="button"
variant="ghost"
@@ -1827,8 +1843,9 @@ export function InviteLayoutCustomizerPanel({
manualZoomRef.current = false;
const fitValue = clampZoom(fitScaleRef.current);
setZoomScale(fitValue);
setPreviewMode('fit');
}}
disabled={Math.abs(effectiveScale - clampZoom(fitScaleRef.current)) < 0.001}
disabled={previewMode === 'full' || Math.abs(effectiveScale - clampZoom(fitScaleRef.current)) < 0.001}
>
{t('invites.customizer.actions.zoomFit', 'Auf Bildschirm')}
</Button>
@@ -1860,9 +1877,12 @@ export function InviteLayoutCustomizerPanel({
<div className="flex justify-center">
<div
ref={designerViewportRef}
className="max-h-[75vh] w-full overflow-auto rounded-3xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-layer)] p-4"
className={cn(
"w-full rounded-3xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-layer)] p-4 overflow-auto",
previewMode === 'full' ? "max-h-none h-[90vh]" : "max-h-[75vh]"
)}
>
<div ref={canvasContainerRef} className="relative flex justify-center">
<div ref={canvasContainerRef} className="relative flex justify-center aspect-[1240/1754] mx-auto max-w-full">
<DesignerCanvas
elements={canvasElements}
selectedId={activeElementId}
@@ -1872,7 +1892,7 @@ export function InviteLayoutCustomizerPanel({
gradient={form.background_gradient ?? activeLayout.preview?.background_gradient ?? null}
accent={form.accent_color ?? activeLayout.preview?.accent ?? '#6366F1'}
text={form.text_color ?? activeLayout.preview?.text ?? '#111827'}
secondary={form.secondary_color ?? activeLayout.preview?.secondary ?? '#1F2937'}
secondary={form.secondary_color ?? '#1F2937'}
badge={form.badge_color ?? form.accent_color ?? '#2563EB'}
qrCodeDataUrl={qrCodeDataUrl}
logoDataUrl={form.logo_data_url ?? form.logo_url ?? null}