layouts schick gemacht und packagelimits weiter implementiert
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user