// import type { EventQrInviteLayout } from '../../api'; // Temporär deaktiviert wegen Modul-Fehler; definiere lokal falls nötig type EventQrInviteLayout = { id: string; name?: string; description?: string | null; subtitle?: string | null; preview?: { background?: string | null; background_gradient?: { angle?: number; stops?: string[] } | null; accent?: string | null; text?: string | null; qr_size_px?: number | null; } | null; formats?: string[]; }; export const CANVAS_WIDTH = 1240; export const CANVAS_HEIGHT = 1754; export type LayoutElementType = | 'qr' | 'headline' | 'subtitle' | 'description' | 'link' | 'badge' | 'logo' | 'cta' | 'text'; export type LayoutTextAlign = 'left' | 'center' | 'right'; export interface LayoutElement { id: string; type: LayoutElementType; x: number; y: number; width: number; height: number; scaleX?: number; scaleY?: number; rotation?: number; fontSize?: number; align?: LayoutTextAlign; content?: string | null; fontFamily?: string | null; letterSpacing?: number; lineHeight?: number; fill?: string | null; locked?: boolean; initial?: boolean; } type PresetValue = number | ((context: LayoutPresetContext) => number); type LayoutPresetElement = { id: string; type: LayoutElementType; x: PresetValue; y: PresetValue; width?: PresetValue; height?: PresetValue; fontSize?: number; align?: LayoutTextAlign; fontFamily?: string; lineHeight?: number; letterSpacing?: number; rotation?: number; locked?: boolean; initial?: boolean; }; type LayoutPreset = LayoutPresetElement[]; interface LayoutPresetContext { qrSize: number; canvasWidth: number; canvasHeight: number; } export interface LayoutElementPayload { id: string; type: LayoutElementType; x: number; y: number; width: number; height: number; scale_x?: number; scale_y?: number; rotation?: number; font_size?: number; align?: LayoutTextAlign; content?: string | null; font_family?: string | null; letter_spacing?: number; line_height?: number; fill?: string | null; locked?: boolean; initial?: boolean; } export interface LayoutSerializationContext { form: QrLayoutCustomization; eventName: string; inviteUrl: string; instructions: string[]; qrSize: number; badgeFallback: string; logoUrl: string | null; } export type QrLayoutCustomization = { layout_id?: string; headline?: string; subtitle?: string; description?: string; badge_label?: string; instructions_heading?: string; instructions?: string[]; link_heading?: string; link_label?: string; cta_label?: string; accent_color?: string; text_color?: string; background_color?: string; secondary_color?: string; badge_color?: string; background_gradient?: { angle?: number; stops?: string[] } | null; logo_data_url?: string | null; logo_url?: string | null; mode?: 'standard' | 'advanced'; elements?: LayoutElementPayload[]; }; export const MIN_QR_SIZE = 400; export const MAX_QR_SIZE = 800; export const MIN_TEXT_WIDTH = 250; export const MIN_TEXT_HEIGHT = 120; export function clamp(value: number, min: number, max: number): number { if (Number.isNaN(value)) { return min; } return Math.min(Math.max(value, min), max); } export function clampElement(element: LayoutElement): LayoutElement { return { ...element, x: clamp(element.x, 20, CANVAS_WIDTH - element.width - 20), y: clamp(element.y, 20, CANVAS_HEIGHT - element.height - 20), width: clamp(element.width, 40, CANVAS_WIDTH - 40), height: clamp(element.height, 40, CANVAS_HEIGHT - 40), scaleX: clamp(element.scaleX ?? 1, 0.1, 5), scaleY: clamp(element.scaleY ?? 1, 0.1, 5), }; } const DEFAULT_TYPE_STYLES: Record = { headline: { width: 900, height: 200, fontSize: 90, align: 'left' }, subtitle: { width: 760, height: 160, fontSize: 44, align: 'left' }, description: { width: 920, height: 320, fontSize: 36, align: 'left' }, link: { width: 520, height: 130, fontSize: 30, align: 'center' }, badge: { width: 420, height: 100, fontSize: 26, align: 'center' }, logo: { width: 320, height: 220, align: 'center' }, cta: { width: 520, height: 130, fontSize: 28, align: 'center' }, qr: { width: 500, height: 500 }, // Default QR significantly larger text: { width: 720, height: 260, fontSize: 28, align: 'left' }, }; 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', x: (c) => (c.canvasWidth - 1000) / 2, y: 350, width: 1000, height: 220, fontSize: 110, align: 'center', fontFamily: 'Playfair Display', lineHeight: 1.3, }, { 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', x: 120, y: 280, width: (context) => context.canvasWidth - 240, height: 200, fontSize: 95, align: 'left', fontFamily: 'Playfair Display', lineHeight: 1.3, }, { id: 'subtitle', type: 'subtitle', x: 120, y: 490, width: 680, height: 140, fontSize: 40, align: 'left', fontFamily: 'Montserrat', lineHeight: 1.4, }, { id: 'description', type: 'description', x: 120, y: 640, width: 680, height: 220, fontSize: 32, align: 'left', fontFamily: 'Lora', lineHeight: 1.5, }, { id: 'qr', type: 'qr', x: (c) => c.canvasWidth - 440 - 120, y: 920, 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', x: (c) => (c.canvasWidth - 1100) / 2, y: 240, width: 1100, height: 220, fontSize: 105, align: 'center', fontFamily: 'Playfair Display', lineHeight: 1.3, }, { id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 900) / 2, y: 480, width: 900, height: 120, fontSize: 40, align: 'center', fontFamily: 'Montserrat', lineHeight: 1.4 }, { id: 'description', type: 'description', x: (c) => (c.canvasWidth - 900) / 2, y: 620, width: 900, height: 180, fontSize: 32, align: 'center', fontFamily: 'Lora', lineHeight: 1.5 }, { id: 'qr', type: 'qr', x: (c) => (c.canvasWidth - 480) / 2, y: 880, 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 }, { id: 'qr', type: 'qr', x: 120, y: 880, 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', x: (c) => c.canvasWidth - 600 - 120, y: 620, width: 600, height: 400, fontSize: 32, align: 'left', fontFamily: 'Lora', lineHeight: 1.6, }, { id: 'link', type: 'link', x: 120, y: (c) => 880 + Math.min(c.qrSize, 460) + 160, width: 460, height: 80, align: 'center', fontSize: 24 }, ]; 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', x: (c) => (c.canvasWidth - 1000) / 2, y: 240, width: 1000, height: 220, fontSize: 100, align: 'center', fontFamily: 'Playfair Display', lineHeight: 1.3, }, { id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 800) / 2, y: 480, width: 800, height: 120, fontSize: 40, align: 'center', fontFamily: 'Montserrat' }, { id: 'description', type: 'description', x: (c) => (c.canvasWidth - 900) / 2, y: 620, width: 900, height: 180, fontSize: 32, align: 'center', fontFamily: 'Lora', lineHeight: 1.5 }, { id: 'qr', type: 'qr', x: (c) => (c.canvasWidth - 480) / 2, y: 880, 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', x: (c) => (c.canvasWidth - 1000) / 2, y: 350, width: 1000, height: 220, fontSize: 110, align: 'center', fontFamily: 'Playfair Display', lineHeight: 1.3, }, { 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 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', x: 120, y: 380, width: 620, height: 380, fontSize: 100, align: 'left', fontFamily: 'Playfair Display', lineHeight: 1.3, }, { id: 'subtitle', type: 'subtitle', x: 120, y: 770, width: 620, height: 140, fontSize: 42, align: 'left', fontFamily: 'Montserrat', lineHeight: 1.4, }, { id: 'description', type: 'description', x: 120, y: 920, width: 620, height: 300, fontSize: 34, align: 'left', fontFamily: 'Lora', lineHeight: 1.5, }, { id: 'qr', type: 'qr', x: (c) => c.canvasWidth - 480 - 120, y: 380, 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' }, ]; const LAYOUT_PRESETS: Record = { 'default': DEFAULT_PRESET, 'evergreen-vows': evergreenVowsPreset, 'midnight-gala': midnightGalaPreset, 'garden-brunch': gardenBrunchPreset, 'sparkler-soiree': sparklerSoireePreset, 'confetti-bash': confettiBashPreset, 'balanced-modern': balancedModernPreset, // New preset: QR right, text left, logo top }; function resolvePresetValue(value: PresetValue | undefined, context: LayoutPresetContext, fallback: number): number { if (typeof value === 'function') { const resolved = value(context); return typeof resolved === 'number' ? resolved : fallback; } if (typeof value === 'number') { return value; } return fallback; } export function buildDefaultElements( layout: EventQrInviteLayout, form: QrLayoutCustomization, eventName: string, qrSize: number ): LayoutElement[] { const size = clamp(qrSize, MIN_QR_SIZE, MAX_QR_SIZE); const context: LayoutPresetContext = { qrSize: size, canvasWidth: CANVAS_WIDTH, canvasHeight: CANVAS_HEIGHT, }; const preset = LAYOUT_PRESETS[layout.id ?? ''] ?? DEFAULT_PRESET; const instructionsHeading = form.instructions_heading ?? layout.instructions_heading ?? "So funktioniert's"; const instructionsList = Array.isArray(form.instructions) && form.instructions.length ? form.instructions : (layout.instructions ?? []); const baseContent: Record = { 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, }; const elements = preset.map((config) => { const typeStyle = DEFAULT_TYPE_STYLES[config.type] ?? { width: 420, height: 160 }; const widthFallback = config.type === 'qr' ? size : typeStyle.width; const heightFallback = config.type === 'qr' ? size : typeStyle.height; const element: LayoutElement = { id: config.id, type: config.type, x: resolvePresetValue(config.x, context, 0), y: resolvePresetValue(config.y, context, 0), width: resolvePresetValue(config.width, context, widthFallback), height: resolvePresetValue(config.height, context, heightFallback), fontSize: config.fontSize ?? typeStyle.fontSize, align: config.align ?? typeStyle.align ?? 'left', fontFamily: config.fontFamily ?? 'Lora', content: null, locked: config.locked ?? typeStyle.locked ?? false, initial: config.initial ?? true, }; if (config.type === 'description') { element.lineHeight = 1.5; } switch (config.id) { case 'headline': element.content = baseContent.headline; break; case 'subtitle': element.content = baseContent.subtitle; break; 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; case 'logo': element.content = form.logo_data_url ?? form.logo_url ?? layout.logo_url ?? null; break; default: if (config.type === 'text') { element.content = element.content ?? 'Individualisiere diesen Textblock mit eigenen Infos.'; } break; } if (config.type === 'qr') { element.locked = false; } const clamped = clampElement(element); return { ...clamped, initial: element.initial ?? true, }; }); return elements; } export function payloadToElements(payload?: LayoutElementPayload[] | null): LayoutElement[] { if (!Array.isArray(payload)) { return []; } return payload.map((entry) => clampElement({ id: entry.id, type: entry.type, x: Number(entry.x ?? 0), y: Number(entry.y ?? 0), width: Number(entry.width ?? 100), height: Number(entry.height ?? 100), scaleX: Number(entry.scale_x ?? 1), scaleY: Number(entry.scale_y ?? 1), rotation: typeof entry.rotation === 'number' ? entry.rotation : 0, fontSize: typeof entry.font_size === 'number' ? entry.font_size : undefined, align: entry.align ?? 'left', content: entry.content ?? null, fontFamily: entry.font_family ?? null, letterSpacing: entry.letter_spacing ?? undefined, lineHeight: entry.line_height ?? undefined, fill: entry.fill ?? null, locked: Boolean(entry.locked), initial: Boolean(entry.initial), }) ); } export function elementsToPayload(elements: LayoutElement[]): LayoutElementPayload[] { return elements.map((element) => ({ id: element.id, type: element.type, x: element.x, y: element.y, width: element.width, height: element.height, scale_x: element.scaleX ?? 1, scale_y: element.scaleY ?? 1, rotation: element.rotation ?? 0, font_size: element.fontSize, align: element.align, content: element.content ?? null, font_family: element.fontFamily ?? null, letter_spacing: element.letterSpacing, line_height: element.lineHeight, fill: element.fill ?? null, locked: element.locked ?? false, initial: element.initial ?? false, })); } export function normalizeElements(elements: LayoutElement[]): LayoutElement[] { const seen = new Set(); return elements .filter((element) => { if (!element.id) { return false; } if (seen.has(element.id)) { return false; } seen.add(element.id); return true; }) .map(clampElement); }