*/ private const LAYOUTS = [ 'modern-poster' => [ 'id' => 'modern-poster', 'name' => 'Modern Poster', 'subtitle' => 'Große, auffällige Fläche – perfekt für den Eingangsbereich.', 'description' => 'Helle Posteroptik mit diagonalem Farbband und deutlicher Call-to-Action.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#F8FAFC', 'text' => '#0F172A', 'accent' => '#6366F1', 'secondary' => '#CBD5F5', 'badge' => '#0EA5E9', 'qr' => ['size_px' => 340], 'svg' => ['width' => 1080, 'height' => 1520], 'instructions' => [ 'Scanne den Code und tritt dem Event direkt bei.', 'Speichere deine Lieblingsmomente mit Foto-Uploads.', 'Merke dir dein Gäste-Pseudonym für Likes und Badges.', ], ], 'elegant-frame' => [ 'id' => 'elegant-frame', 'name' => 'Elegant Frame', 'subtitle' => 'Ein ruhiges Layout mit Fokus auf Eleganz.', 'description' => 'Serifen-Schrift, pastellige Flächen und dezente Rahmen für elegante Anlässe.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#FBF7F2', 'text' => '#2B1B13', 'accent' => '#C08457', 'secondary' => '#E6D5C3', 'badge' => '#8B5CF6', 'qr' => ['size_px' => 300], 'svg' => ['width' => 1080, 'height' => 1520], 'instructions' => [ 'QR-Code scannen oder Link im Browser eingeben.', 'Name eingeben, Lieblingssprache auswählen und loslegen.', 'Zeige diesen Druck am Empfang als Orientierung für Gäste.', ], ], 'bold-gradient' => [ 'id' => 'bold-gradient', 'name' => 'Bold Gradient', 'subtitle' => 'Farbverlauf mit starkem Kontrast.', 'description' => 'Ein kraftvolles Farbstatement mit großem QR-Code – ideal für Partys.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#F97316', 'background_gradient' => [ 'angle' => 190, 'stops' => ['#F97316', '#EC4899', '#8B5CF6'], ], 'text' => '#FFFFFF', 'accent' => '#FFFFFF', 'secondary' => 'rgba(255,255,255,0.72)', 'badge' => '#1E293B', 'qr' => ['size_px' => 360], 'svg' => ['width' => 1080, 'height' => 1520], 'instructions' => [ 'Sofort scannen – der QR-Code führt direkt zum Event.', 'Fotos knipsen, Challenges lösen und Likes sammeln.', 'Teile den Link mit Freund:innen, falls kein Scan möglich ist.', ], ], 'photo-strip' => [ 'id' => 'photo-strip', 'name' => 'Photo Strip', 'subtitle' => 'Layout mit Fotostreifen-Anmutung und Checkliste.', 'description' => 'Horizontale Teilung, Platz für Hinweise und Storytelling.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#FFFFFF', 'text' => '#111827', 'accent' => '#0EA5E9', 'secondary' => '#94A3B8', 'badge' => '#334155', 'qr' => ['size_px' => 320], 'svg' => ['width' => 1080, 'height' => 1520], 'instructions' => [ 'Schritt 1: QR-Code scannen oder Kurzlink nutzen.', 'Schritt 2: Profilname eingeben – kreativ sein!', 'Schritt 3: Fotos hochladen und Teamaufgaben lösen.', ], ], 'minimal-card' => [ 'id' => 'minimal-card', 'name' => 'Minimal Card', 'subtitle' => 'Kleine Karte – mehrfach druckbar als Tischaufsteller.', 'description' => 'Schlichtes Kartenformat mit klarer Typografie und viel Weißraum.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#F9FAFB', 'text' => '#111827', 'accent' => '#9333EA', 'secondary' => '#E0E7FF', 'badge' => '#64748B', 'qr' => ['size_px' => 280], 'svg' => ['width' => 1080, 'height' => 1520], 'instructions' => [ 'Code scannen, Profil erstellen, Erinnerungen festhalten.', 'Halte diese Karte an mehreren Stellen bereit.', 'Für Ausdrucke auf 200 g/m² Kartenpapier empfohlen.', ], ], ]; /** * Get layout definitions. * * @return array> */ public static function all(): array { $customLayouts = InviteLayout::query() ->where('is_active', true) ->orderBy('name') ->get(); if ($customLayouts->isNotEmpty()) { return $customLayouts ->map(fn (InviteLayout $layout) => self::normalize(self::fromModel($layout))) ->values() ->all(); } return array_values(array_map(fn ($layout) => self::normalize($layout), self::LAYOUTS)); } /** * Find a layout definition. */ public static function find(string $id): ?array { $custom = InviteLayout::query() ->where('slug', $id) ->where('is_active', true) ->first(); if ($custom) { return self::normalize(self::fromModel($custom)); } $layout = self::LAYOUTS[$id] ?? null; return $layout ? self::normalize($layout) : null; } /** * Normalize and merge default values. */ private static function normalize(array $layout): array { $defaults = [ 'subtitle' => '', 'description' => '', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#F9FAFB', 'text' => '#0F172A', 'accent' => '#6366F1', 'secondary' => '#CBD5F5', 'badge' => '#2563EB', 'badge_label' => 'Digitale Gästebox', 'instructions_heading' => "So funktioniert's", 'link_heading' => 'Alternative zum Einscannen', 'cta_label' => 'Scan mich & starte direkt', 'cta_caption' => 'Scan mich & starte direkt', 'link_label' => null, 'logo_url' => null, 'qr' => [ 'size_px' => 320, ], 'svg' => [ 'width' => 1080, 'height' => 1520, ], 'background_gradient' => null, 'instructions' => [], 'formats' => ['pdf', 'svg'], ]; return array_replace_recursive($defaults, $layout); } private static function fromModel(InviteLayout $layout): array { $preview = $layout->preview ?? []; $options = $layout->layout_options ?? []; $instructions = $layout->instructions ?? []; return array_filter([ 'id' => $layout->slug, 'name' => $layout->name, 'subtitle' => $layout->subtitle, 'description' => $layout->description, 'paper' => $layout->paper, 'orientation' => $layout->orientation, 'background' => $preview['background'] ?? null, 'background_gradient' => $preview['background_gradient'] ?? null, 'text' => $preview['text'] ?? null, 'accent' => $preview['accent'] ?? null, 'secondary' => $preview['secondary'] ?? null, 'badge' => $preview['badge'] ?? null, 'badge_label' => $options['badge_label'] ?? null, 'instructions_heading' => $options['instructions_heading'] ?? null, 'link_heading' => $options['link_heading'] ?? null, 'cta_label' => $options['cta_label'] ?? null, 'cta_caption' => $options['cta_caption'] ?? null, 'link_label' => $options['link_label'] ?? null, 'logo_url' => $options['logo_url'] ?? null, 'qr' => array_filter([ 'size_px' => $preview['qr']['size_px'] ?? $options['qr']['size_px'] ?? $preview['qr_size_px'] ?? $options['qr_size_px'] ?? null, ]), 'svg' => array_filter([ 'width' => $preview['svg']['width'] ?? $options['svg']['width'] ?? $preview['svg_width'] ?? $options['svg_width'] ?? null, 'height' => $preview['svg']['height'] ?? $options['svg']['height'] ?? $preview['svg_height'] ?? $options['svg_height'] ?? null, ]), 'formats' => $options['formats'] ?? ['pdf', 'svg'], 'instructions' => $instructions, ], fn ($value) => $value !== null && $value !== []); } /** * Map layouts into an API-ready response structure, attaching URLs. * * @param callable(string $layoutId, string $format): string $urlResolver * @return array> */ public static function toResponse(callable $urlResolver): array { return array_map(function (array $layout) use ($urlResolver) { $formats = $layout['formats'] ?? ['pdf', 'svg']; return [ 'id' => $layout['id'], 'name' => $layout['name'], 'description' => $layout['description'], 'subtitle' => $layout['subtitle'], 'preview' => [ 'background' => $layout['background'], 'background_gradient' => $layout['background_gradient'], 'accent' => $layout['accent'], 'text' => $layout['text'], ], 'formats' => $formats, 'download_urls' => collect($formats) ->mapWithKeys(fn ($format) => [$format => $urlResolver($layout['id'], $format)]) ->all(), ]; }, self::all()); } }