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);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
export type BackgroundImageOption = {
|
||||
id: string;
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
// Preload background assets from public/storage/layouts/backgrounds.
|
||||
// Vite does not process the public directory, so we try a glob (for cases where assets are in src)
|
||||
// and fall back to known public URLs.
|
||||
const backgroundImports: Record<string, string> = {
|
||||
...import.meta.glob('../../../../../public/storage/layouts/backgrounds/*.{jpg,jpeg,png,webp,avif}', {
|
||||
eager: true,
|
||||
as: 'url',
|
||||
}),
|
||||
...import.meta.glob('/storage/layouts/backgrounds/*.{jpg,jpeg,png,webp,avif}', {
|
||||
eager: true,
|
||||
as: 'url',
|
||||
}),
|
||||
};
|
||||
|
||||
const fallbackFiles = ['bg-blue-floral.png', 'bg-goldframe.png', 'gr-green-floral.png'];
|
||||
|
||||
const importedBackgrounds: BackgroundImageOption[] = Object.entries(backgroundImports).map(([path, url]) => {
|
||||
const filename = path.split('/').pop() ?? path;
|
||||
const id = filename.replace(/\.[^.]+$/, '');
|
||||
return { id, url: url as string, label: filename };
|
||||
});
|
||||
|
||||
const fallbackBackgrounds: BackgroundImageOption[] = fallbackFiles.map((filename) => ({
|
||||
id: filename.replace(/\.[^.]+$/, ''),
|
||||
url: `/storage/layouts/backgrounds/${filename}`,
|
||||
label: filename,
|
||||
}));
|
||||
|
||||
const merged = [...importedBackgrounds, ...fallbackBackgrounds];
|
||||
|
||||
export const preloadedBackgrounds: BackgroundImageOption[] = Array.from(
|
||||
merged.reduce((map, item) => {
|
||||
if (!map.has(item.id)) {
|
||||
map.set(item.id, item);
|
||||
}
|
||||
return map;
|
||||
}, new Map<string, BackgroundImageOption>()),
|
||||
).map(([, value]) => value);
|
||||
@@ -127,6 +127,7 @@ export type QrLayoutCustomization = {
|
||||
secondary_color?: string;
|
||||
badge_color?: string;
|
||||
background_gradient?: { angle?: number; stops?: string[] } | null;
|
||||
background_image?: string | null;
|
||||
logo_data_url?: string | null;
|
||||
logo_url?: string | null;
|
||||
mode?: 'standard' | 'advanced';
|
||||
@@ -172,7 +173,6 @@ const DEFAULT_TYPE_STYLES: Record<LayoutElementType, { width: number; height: nu
|
||||
const DEFAULT_PRESET: LayoutPreset = [
|
||||
// Basierend auf dem zentrierten, modernen "confetti-bash"-Layout
|
||||
{ id: 'logo', type: 'logo', x: (c) => (c.canvasWidth - 280) / 2, y: 80, width: 280, height: 140, align: 'center' },
|
||||
{ id: 'badge', type: 'badge', x: (c) => (c.canvasWidth - 520) / 2, y: 240, width: 520, height: 90, align: 'center', fontSize: 28, lineHeight: 1.4, letterSpacing: 0.5, fontFamily: 'Montserrat' },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
@@ -188,13 +188,11 @@ const DEFAULT_PRESET: LayoutPreset = [
|
||||
{ id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 800) / 2, y: 580, width: 800, height: 120, fontSize: 42, align: 'center', fontFamily: 'Montserrat', lineHeight: 1.4 },
|
||||
{ id: 'description', type: 'description', x: (c) => (c.canvasWidth - 900) / 2, y: 720, width: 900, height: 180, fontSize: 34, align: 'center', fontFamily: 'Lora', lineHeight: 1.5 },
|
||||
{ id: 'qr', type: 'qr', x: (c) => (c.canvasWidth - 500) / 2, y: 940, width: (c) => Math.min(c.qrSize, 500), height: (c) => Math.min(c.qrSize, 500) },
|
||||
{ id: 'cta', type: 'cta', x: (c) => (c.canvasWidth - 600) / 2, y: (c) => 940 + Math.min(c.qrSize, 500) + 40, width: 600, height: 100, align: 'center', fontSize: 32, fontFamily: 'Montserrat', lineHeight: 1.4 },
|
||||
{ id: 'link', type: 'link', x: (c) => (c.canvasWidth - 700) / 2, y: (c) => 940 + Math.min(c.qrSize, 500) + 160, width: 700, height: 80, align: 'center', fontSize: 26, fontFamily: 'Montserrat', lineHeight: 1.5 },
|
||||
];
|
||||
const evergreenVowsPreset: LayoutPreset = [
|
||||
// Elegant, linksbündig mit verbesserter Balance
|
||||
{ id: 'logo', type: 'logo', x: 120, y: 100, width: 300, height: 150, align: 'left' },
|
||||
{ id: 'badge', type: 'badge', x: (c) => c.canvasWidth - 520 - 120, y: 125, width: 520, height: 90, align: 'right', fontSize: 28, lineHeight: 1.4, fontFamily: 'Montserrat' },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
@@ -239,13 +237,11 @@ const evergreenVowsPreset: LayoutPreset = [
|
||||
width: (c) => Math.min(c.qrSize, 440),
|
||||
height: (c) => Math.min(c.qrSize, 440),
|
||||
},
|
||||
{ id: 'cta', type: 'cta', x: (c) => c.canvasWidth - 440 - 120, y: (c) => 920 + Math.min(c.qrSize, 440) + 40, width: 440, height: 100, align: 'center', fontSize: 30, fontFamily: 'Montserrat', lineHeight: 1.4 },
|
||||
{ id: 'link', type: 'link', x: (c) => c.canvasWidth - 440 - 120, y: (c) => 920 + Math.min(c.qrSize, 440) + 160, width: 440, height: 80, align: 'center', fontSize: 24, fontFamily: 'Montserrat' },
|
||||
];
|
||||
|
||||
const midnightGalaPreset: LayoutPreset = [
|
||||
// Zentriert, premium, mehr vertikaler Abstand
|
||||
{ id: 'badge', type: 'badge', x: (c) => (c.canvasWidth - 560) / 2, y: 120, width: 560, height: 90, align: 'center', fontSize: 28, fontFamily: 'Montserrat' },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
@@ -268,13 +264,11 @@ const midnightGalaPreset: LayoutPreset = [
|
||||
width: (c) => Math.min(c.qrSize, 480),
|
||||
height: (c) => Math.min(c.qrSize, 480),
|
||||
},
|
||||
{ id: 'cta', type: 'cta', x: (c) => (c.canvasWidth - 520) / 2, y: (c) => 880 + Math.min(c.qrSize, 480) + 40, width: 520, height: 100, align: 'center', fontSize: 30, fontFamily: 'Montserrat', lineHeight: 1.4 },
|
||||
{ id: 'link', type: 'link', x: (c) => (c.canvasWidth - 600) / 2, y: (c) => 880 + Math.min(c.qrSize, 480) + 160, width: 600, height: 80, align: 'center', fontSize: 24, fontFamily: 'Montserrat' },
|
||||
];
|
||||
|
||||
const gardenBrunchPreset: LayoutPreset = [
|
||||
// Verspielt, asymmetrisch, aber ausbalanciert
|
||||
{ id: 'badge', type: 'badge', x: 120, y: 120, width: 500, height: 90, align: 'left', fontSize: 28, fontFamily: 'Montserrat' },
|
||||
{ id: 'headline', type: 'headline', x: 120, y: 240, width: 900, height: 200, fontSize: 90, align: 'left', fontFamily: 'Playfair Display', lineHeight: 1.3 },
|
||||
{ id: 'subtitle', type: 'subtitle', x: 120, y: 450, width: 700, height: 120, fontSize: 40, align: 'left', fontFamily: 'Montserrat', lineHeight: 1.4 },
|
||||
{
|
||||
@@ -285,7 +279,6 @@ const gardenBrunchPreset: LayoutPreset = [
|
||||
width: (c) => Math.min(c.qrSize, 460),
|
||||
height: (c) => Math.min(c.qrSize, 460),
|
||||
},
|
||||
{ id: 'cta', type: 'cta', x: 120, y: (c) => 880 + Math.min(c.qrSize, 460) + 40, width: 460, height: 100, align: 'center', fontSize: 30, fontFamily: 'Montserrat' },
|
||||
{
|
||||
id: 'description',
|
||||
type: 'description',
|
||||
@@ -303,7 +296,6 @@ const gardenBrunchPreset: LayoutPreset = [
|
||||
|
||||
const sparklerSoireePreset: LayoutPreset = [
|
||||
// Festlich, zentriert, klar
|
||||
{ id: 'badge', type: 'badge', x: (c) => (c.canvasWidth - 560) / 2, y: 120, width: 560, height: 90, align: 'center', fontSize: 28, fontFamily: 'Montserrat' },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
@@ -326,14 +318,12 @@ const sparklerSoireePreset: LayoutPreset = [
|
||||
width: (c) => Math.min(c.qrSize, 480),
|
||||
height: (c) => Math.min(c.qrSize, 480),
|
||||
},
|
||||
{ id: 'cta', type: 'cta', x: (c) => (c.canvasWidth - 520) / 2, y: (c) => 880 + Math.min(c.qrSize, 480) + 40, width: 520, height: 100, align: 'center', fontSize: 30, fontFamily: 'Montserrat' },
|
||||
{ id: 'link', type: 'link', x: (c) => (c.canvasWidth - 600) / 2, y: (c) => 880 + Math.min(c.qrSize, 480) + 160, width: 600, height: 80, align: 'center', fontSize: 24, fontFamily: 'Montserrat' },
|
||||
];
|
||||
|
||||
const confettiBashPreset: LayoutPreset = [
|
||||
// Zentriertes, luftiges Layout mit klarer Hierarchie.
|
||||
{ id: 'logo', type: 'logo', x: (c) => (c.canvasWidth - 280) / 2, y: 80, width: 280, height: 140, align: 'center' },
|
||||
{ id: 'badge', type: 'badge', x: (c) => (c.canvasWidth - 520) / 2, y: 240, width: 520, height: 90, align: 'center', fontSize: 28, lineHeight: 1.4, letterSpacing: 0.5, fontFamily: 'Montserrat' },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
@@ -378,18 +368,6 @@ const confettiBashPreset: LayoutPreset = [
|
||||
width: (c) => Math.min(c.qrSize, 500),
|
||||
height: (c) => Math.min(c.qrSize, 500),
|
||||
},
|
||||
{
|
||||
id: 'cta',
|
||||
type: 'cta',
|
||||
x: (c) => (c.canvasWidth - 600) / 2,
|
||||
y: (c) => 940 + Math.min(c.qrSize, 500) + 40,
|
||||
width: 600,
|
||||
height: 100,
|
||||
align: 'center',
|
||||
fontSize: 32,
|
||||
fontFamily: 'Montserrat',
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
type: 'link',
|
||||
@@ -407,7 +385,6 @@ const confettiBashPreset: LayoutPreset = [
|
||||
const balancedModernPreset: LayoutPreset = [
|
||||
// Wahrhaftig balanciert: Text links, QR rechts
|
||||
{ id: 'logo', type: 'logo', x: 120, y: 100, width: 300, height: 150, align: 'left' },
|
||||
{ id: 'badge', type: 'badge', x: 120, y: 270, width: 500, height: 90, align: 'left', fontSize: 28, fontFamily: 'Montserrat' },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
@@ -452,7 +429,6 @@ const balancedModernPreset: LayoutPreset = [
|
||||
width: 480,
|
||||
height: 480,
|
||||
},
|
||||
{ id: 'cta', type: 'cta', x: (c) => c.canvasWidth - 480 - 120, y: 880, width: 480, height: 100, align: 'center', fontSize: 30, fontFamily: 'Montserrat' },
|
||||
{ id: 'link', type: 'link', x: (c) => c.canvasWidth - 480 - 120, y: 1000, width: 480, height: 80, align: 'center', fontSize: 24, fontFamily: 'Montserrat' },
|
||||
];
|
||||
|
||||
@@ -501,9 +477,7 @@ export function buildDefaultElements(
|
||||
headline: form.headline ?? eventName,
|
||||
subtitle: form.subtitle ?? layout.subtitle ?? '',
|
||||
description: form.description ?? layout.description ?? '',
|
||||
badge: form.badge_label ?? layout.badge_label ?? 'Digitale Gästebox',
|
||||
link: form.link_label ?? '',
|
||||
cta: form.cta_label ?? layout.cta_label ?? 'Scan mich & starte direkt',
|
||||
instructions_heading: instructionsHeading,
|
||||
instructions_text: instructionsList[0] ?? null,
|
||||
};
|
||||
@@ -541,15 +515,9 @@ export function buildDefaultElements(
|
||||
case 'description':
|
||||
element.content = baseContent.description;
|
||||
break;
|
||||
case 'badge':
|
||||
element.content = baseContent.badge;
|
||||
break;
|
||||
case 'link':
|
||||
element.content = baseContent.link;
|
||||
break;
|
||||
case 'cta':
|
||||
element.content = baseContent.cta;
|
||||
break;
|
||||
case 'text-strip':
|
||||
element.content = instructionsList.join('\n').trim() || layout.description || 'Nutze diesen Bereich für zusätzliche Hinweise oder Storytelling.';
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user