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

@@ -22,6 +22,7 @@ type DesignerCanvasProps = {
badge: string;
qrCodeDataUrl: string | null;
logoDataUrl: string | null;
backgroundImageUrl?: string | null;
scale?: number;
readOnly?: boolean;
layoutKey?: string;
@@ -36,6 +37,7 @@ export function DesignerCanvas({
onChange,
background,
gradient,
backgroundImageUrl = null,
accent,
text,
secondary,
@@ -233,6 +235,8 @@ export function DesignerCanvas({
return;
}
const elementId = target.elementId;
const action = event.transform?.action ?? null;
const isScalingAction = action?.startsWith('scale') || action === 'resize';
const bounds = target.getBoundingRect();
const nextPatch: Partial<LayoutElement> = {
@@ -240,61 +244,55 @@ export function DesignerCanvas({
y: clamp(Math.round(bounds.top ?? 0), 20, CANVAS_HEIGHT - 20),
};
// Manual collision check: Calculate overlap and push vertically
const otherObjects = canvas
.getObjects()
.filter((obj): obj is FabricObjectWithId => obj !== target && Boolean((obj as FabricObjectWithId).elementId));
otherObjects.forEach((other) => {
const otherBounds = other.getBoundingRect();
const overlapX = Math.max(0, Math.min(bounds.left + bounds.width, otherBounds.left + otherBounds.width) - Math.max(bounds.left, otherBounds.left));
const overlapY = Math.max(0, Math.min(bounds.top + bounds.height, otherBounds.top + otherBounds.height) - Math.max(bounds.top, otherBounds.top));
if (overlapX > 0 && overlapY > 0) {
// Push down by 120px if overlap (massive spacing für größeren QR-Code)
nextPatch.y = Math.max(nextPatch.y ?? 0, (Number(otherBounds.top || 0)) + (Number(otherBounds.height || 0)) + 120);
}
});
const isImage = target.type === 'image';
if (isImage) {
const currentScaleX = target.scaleX ?? 1;
const currentScaleY = target.scaleY ?? 1;
const naturalWidth = target.width ?? 0;
const naturalHeight = target.height ?? 0;
if (elementId === 'qr') {
// For QR: Enforce uniform scale, cap size, padding=0
const avgScale = (currentScaleX + currentScaleY) / 2;
const cappedSize = Math.min(Math.round(naturalWidth * avgScale), 800); // Cap at 800px for massive QR
nextPatch.width = cappedSize;
nextPatch.height = cappedSize;
nextPatch.scaleX = cappedSize / naturalWidth;
nextPatch.scaleY = cappedSize / naturalHeight;
if (isScalingAction) {
if (isImage) {
const currentScaleX = target.scaleX ?? 1;
const currentScaleY = target.scaleY ?? 1;
const naturalWidth = target.width ?? 0;
const naturalHeight = target.height ?? 0;
if (elementId === 'qr') {
// For QR: Enforce uniform scale, cap size, padding=0
const avgScale = (currentScaleX + currentScaleY) / 2;
const cappedSize = Math.min(Math.round(naturalWidth * avgScale), 800); // Cap at 800px for massive QR
nextPatch.width = cappedSize;
nextPatch.height = cappedSize;
nextPatch.scaleX = cappedSize / naturalWidth;
nextPatch.scaleY = cappedSize / naturalHeight;
target.set({
left: nextPatch.x,
top: nextPatch.y,
scaleX: nextPatch.scaleX,
scaleY: nextPatch.scaleY,
padding: 12, // Increased padding for better frame visibility
uniformScaling: true, // Lock aspect ratio
lockScalingFlip: true,
});
} else {
nextPatch.width = Math.round(naturalWidth * currentScaleX);
nextPatch.height = Math.round(naturalHeight * currentScaleY);
nextPatch.scaleX = currentScaleX;
nextPatch.scaleY = currentScaleY;
target.set({ left: nextPatch.x, top: nextPatch.y, padding: 10 });
}
} else {
nextPatch.width = clamp(Math.round(bounds.width ?? target.width ?? 0), 40, CANVAS_WIDTH - 40);
nextPatch.height = clamp(Math.round(bounds.height ?? target.height ?? 0), 40, CANVAS_HEIGHT - 40);
target.set({
scaleX: 1,
scaleY: 1,
left: nextPatch.x,
top: nextPatch.y,
scaleX: nextPatch.scaleX,
scaleY: nextPatch.scaleY,
padding: 12, // Increased padding for better frame visibility
uniformScaling: true, // Lock aspect ratio
lockScalingFlip: true,
width: nextPatch.width,
height: nextPatch.height,
padding: 10, // Default padding for text
});
} else {
nextPatch.width = Math.round(naturalWidth * currentScaleX);
nextPatch.height = Math.round(naturalHeight * currentScaleY);
nextPatch.scaleX = currentScaleX;
nextPatch.scaleY = currentScaleY;
target.set({ left: nextPatch.x, top: nextPatch.y, padding: 10 });
}
} else {
nextPatch.width = clamp(Math.round(bounds.width ?? target.width ?? 0), 40, CANVAS_WIDTH - 40);
nextPatch.height = clamp(Math.round(bounds.height ?? target.height ?? 0), 40, CANVAS_HEIGHT - 40);
// Dragging: keep size, only move
target.set({
scaleX: 1,
scaleY: 1,
left: nextPatch.x,
top: nextPatch.y,
width: nextPatch.width,
height: nextPatch.height,
padding: 10, // Default padding for text
});
}
@@ -349,6 +347,7 @@ export function DesignerCanvas({
logoDataUrl,
background,
gradient,
backgroundImageUrl,
readOnly,
});
@@ -367,6 +366,7 @@ export function DesignerCanvas({
logoDataUrl,
backgroundColor: background,
backgroundGradient: gradient,
backgroundImageUrl,
readOnly,
}).catch((error) => {
console.error('[Fabric] Failed to render layout', error);
@@ -381,6 +381,7 @@ export function DesignerCanvas({
logoDataUrl,
background,
gradient,
backgroundImageUrl,
readOnly,
]);
@@ -456,6 +457,7 @@ export type FabricRenderOptions = {
logoDataUrl: string | null;
backgroundColor: string;
backgroundGradient: { angle?: number; stops?: string[] } | null;
backgroundImageUrl?: string | null;
readOnly: boolean;
};
@@ -473,13 +475,21 @@ export async function renderFabricLayout(
logoDataUrl,
backgroundColor,
backgroundGradient,
backgroundImageUrl,
readOnly,
} = options;
canvas.discardActiveObject();
// Aggressively clear previous objects/state to avoid stacking duplicates between renders.
try {
const existing = canvas.getObjects();
existing.forEach((obj) => canvas.remove(obj));
} catch (error) {
console.warn('[Invites][Fabric] failed to remove existing objects', error);
}
canvas.clear();
applyBackground(canvas, backgroundColor, backgroundGradient);
await applyBackground(canvas, backgroundColor, backgroundGradient, backgroundImageUrl);
console.debug('[Invites][Fabric] render', {
elementCount: elements.length,
@@ -543,11 +553,70 @@ export async function renderFabricLayout(
canvas.renderAll();
}
export function applyBackground(
export async function applyBackground(
canvas: fabric.Canvas,
color: string,
gradient: { angle?: number; stops?: string[] } | null,
): void {
backgroundImageUrl?: string | null,
): Promise<void> {
try {
if (typeof canvas.setBackgroundImage === 'function') {
canvas.setBackgroundImage(null, canvas.requestRenderAll.bind(canvas));
} else {
// Fallback for environments where setBackgroundImage is not present
(canvas as fabric.StaticCanvas).backgroundImage = null;
canvas.requestRenderAll();
}
if (backgroundImageUrl) {
try {
const resolvedUrl = backgroundImageUrl.startsWith('http')
? backgroundImageUrl
: `${window.location.origin}${backgroundImageUrl.startsWith('/') ? '' : '/'}${backgroundImageUrl}`;
const image = await new Promise<fabric.Image | null>((resolve) => {
const imgEl = new Image();
imgEl.crossOrigin = 'anonymous';
const timeoutId = window.setTimeout(() => resolve(null), 3000);
imgEl.onload = () => {
window.clearTimeout(timeoutId);
resolve(new fabric.Image(imgEl, { crossOrigin: 'anonymous' }));
};
imgEl.onerror = () => {
window.clearTimeout(timeoutId);
resolve(null);
};
imgEl.src = resolvedUrl;
});
if (image) {
const scaleX = CANVAS_WIDTH / (image.width || CANVAS_WIDTH);
const scaleY = CANVAS_HEIGHT / (image.height || CANVAS_HEIGHT);
const scale = Math.max(scaleX, scaleY);
image.set({
originX: 'left',
originY: 'top',
left: 0,
top: 0,
scaleX: scale,
scaleY: scale,
selectable: false,
evented: false,
});
if (typeof canvas.setBackgroundImage === 'function') {
canvas.setBackgroundImage(image, canvas.requestRenderAll.bind(canvas));
} else {
(canvas as fabric.StaticCanvas).backgroundImage = image;
canvas.requestRenderAll();
}
return;
}
} catch (error) {
console.warn('[Fabric] Failed to load background image', error);
}
}
} catch (error) {
console.warn('[Fabric] applyBackground failed', error);
}
let background: string | fabric.Gradient<'linear'> = color;
if (gradient?.stops?.length) {
@@ -821,15 +890,34 @@ export async function loadImageObject(
const intrinsicWidth = image.width ?? element.width;
const intrinsicHeight = image.height ?? element.height;
const scaleX = element.width / intrinsicWidth;
const scaleY = element.height / intrinsicHeight;
const safeIntrinsicWidth = intrinsicWidth || 1;
const safeIntrinsicHeight = intrinsicHeight || 1;
let targetLeft = element.x;
let targetTop = element.y;
let scaleX = element.width / safeIntrinsicWidth;
let scaleY = element.height / safeIntrinsicHeight;
if (options?.objectFit === 'contain') {
const ratio = Math.min(scaleX, scaleY);
scaleX = ratio;
scaleY = ratio;
const renderedWidth = safeIntrinsicWidth * ratio;
const renderedHeight = safeIntrinsicHeight * ratio;
targetLeft = element.x + (element.width - renderedWidth) / 2;
targetTop = element.y + (element.height - renderedHeight) / 2;
}
image.set({
...baseConfig,
width: element.width,
height: element.height,
originX: 'left',
originY: 'top',
width: safeIntrinsicWidth,
height: safeIntrinsicHeight,
scaleX,
scaleY,
left: targetLeft,
top: targetTop,
padding: options?.padding ?? 0,
});
@@ -837,16 +925,6 @@ export async function loadImageObject(
image.set('shadow', options.shadow);
}
if (options?.objectFit === 'contain') {
const ratio = Math.min(scaleX, scaleY);
image.set({
scaleX: ratio,
scaleY: ratio,
left: element.x + (element.width - intrinsicWidth * ratio) / 2,
top: element.y + (element.height - intrinsicHeight * ratio) / 2,
});
}
resolveSafely(image);
};