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