*/ private const LAYOUTS = [ 'evergreen-vows' => [ 'id' => 'evergreen-vows', 'name' => 'Evergreen Vows', 'subtitle' => 'Romantische Einladung für Trauung & Empfang.', 'description' => 'Weiche Pastelltöne, florale Akzente und viel Raum für eine herzliche Begrüßung.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#FBF7F2', 'background_gradient' => [ 'angle' => 165, 'stops' => ['#FBF7F2', '#FDECEF', '#F4F0FF'], ], 'text' => '#2C1A27', 'accent' => '#B85C76', 'secondary' => '#E7D6DC', 'badge' => '#7A9375', 'badge_label' => 'Unsere Gästegalerie', 'instructions_heading' => 'So seid ihr dabei', 'link_heading' => 'Falls der Scan nicht klappt', 'cta_label' => 'Gästegalerie öffnen', 'cta_caption' => 'Jetzt Erinnerungen sammeln', 'qr' => ['size_px' => 640], 'svg' => ['width' => 1240, 'height' => 1754], 'instructions' => [ 'QR-Code scannen und mit eurem Lieblingsnamen anmelden.', 'Ein paar Schnappschüsse teilen – gern auch Behind-the-Scenes!', 'Likes vergeben und Grüße für das Brautpaar schreiben.', ], ], 'midnight-gala' => [ 'id' => 'midnight-gala', 'name' => 'Midnight Gala', 'subtitle' => 'Eleganter Auftritt für Corporate Events & Galas.', 'description' => 'Dunkle Bühne mit goldenen Akzenten und kräftiger Typografie.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#0B132B', 'background_gradient' => [ 'angle' => 200, 'stops' => ['#0B132B', '#1C2541', '#274690'], ], 'text' => '#F8FAFC', 'accent' => '#F9C74F', 'secondary' => '#4E5D8F', 'badge' => '#F94144', 'badge_label' => 'Team Lounge Access', 'instructions_heading' => 'In drei Schritten bereit', 'link_heading' => 'Link teilen statt scannen', 'cta_label' => 'Jetzt Event-Hub öffnen', 'cta_caption' => 'Programm, Uploads & Highlights', 'qr' => ['size_px' => 640], 'svg' => ['width' => 1240, 'height' => 1754], 'instructions' => [ 'QR-Code scannen oder Kurzlink eingeben.', 'Mit Firmen-E-Mail anmelden und Zugang bestätigen.', 'Agenda verfolgen, Fotos teilen und Highlights voten.', ], ], 'garden-brunch' => [ 'id' => 'garden-brunch', 'name' => 'Garden Brunch', 'subtitle' => 'Luftiges Layout für Tages-Events & Familienfeiern.', 'description' => 'Sanfte Grüntöne, natürliche Formen und Platz für Hinweise.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#F6F9F4', 'background_gradient' => [ 'angle' => 120, 'stops' => ['#F6F9F4', '#EEF5E7', '#F8FAF0'], ], 'text' => '#2F4030', 'accent' => '#6BAA75', 'secondary' => '#DDE9D8', 'badge' => '#F1C376', 'badge_label' => 'Brunch Fotostation', 'instructions_heading' => 'So funktioniert’s', 'link_heading' => 'Alternativ zum Scannen', 'cta_label' => 'Gästebuch öffnen', 'cta_caption' => 'Eure Grüße festhalten', 'qr' => ['size_px' => 660], 'svg' => ['width' => 1240, 'height' => 1754], 'instructions' => [ 'QR-Code scannen und Namen eintragen.', 'Lieblingsfoto hochladen oder neue Momente festhalten.', 'Aufgaben ausprobieren und anderen ein Herz dalassen.', ], ], 'sparkler-soiree' => [ 'id' => 'sparkler-soiree', 'name' => 'Sparkler Soirée', 'subtitle' => 'Abendliches Layout mit funkelndem Verlauf.', 'description' => 'Dynamische Typografie mit zentralem Fokus auf dem QR-Code.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#1B1A44', 'background_gradient' => [ 'angle' => 205, 'stops' => ['#1B1A44', '#42275A', '#734B8F'], ], 'text' => '#FDF7FF', 'accent' => '#F9A826', 'secondary' => '#DDB7FF', 'badge' => '#FF6F61', 'badge_label' => 'Night Shots', 'instructions_heading' => 'Step-by-Step', 'link_heading' => 'QR funktioniert nicht?', 'cta_label' => 'Partyfeed starten', 'cta_caption' => 'Momente live teilen', 'qr' => ['size_px' => 680], 'svg' => ['width' => 1240, 'height' => 1754], 'instructions' => [ 'Code scannen und kurz registrieren.', 'Spotlights & Challenges entdecken.', 'Fotos hochladen und die besten Shots voten.', ], ], 'confetti-bash' => [ 'id' => 'confetti-bash', 'name' => 'Confetti Bash', 'subtitle' => 'Verspielter Look für Geburtstage & Jubiläen.', 'description' => 'Konfetti-Sprenkel, fröhliche Farben und viel Platz für Hinweise.', 'paper' => 'a4', 'orientation' => 'portrait', 'background' => '#FFF9F0', 'background_gradient' => [ 'angle' => 145, 'stops' => ['#FFF9F0', '#FFEFEF', '#FFF5D6'], ], 'text' => '#31291F', 'accent' => '#FF6F61', 'secondary' => '#F9D6A5', 'badge' => '#4E88FF', 'badge_label' => 'Party-Schnappschüsse', 'instructions_heading' => 'Leg direkt los', 'link_heading' => 'Kurzlink für Gäste', 'cta_label' => 'Zur Geburtstagswand', 'cta_caption' => 'Fotos & Grüße posten', 'qr' => ['size_px' => 680], 'svg' => ['width' => 1240, 'height' => 1754], 'instructions' => [ 'QR-Code scannen und Wunschname auswählen.', 'Dein erstes Foto oder Video hochladen.', 'Freunde einladen, Likes vergeben und gemeinsam feiern!', ], ], ]; /** * 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' => 640, ], 'svg' => [ 'width' => 1240, 'height' => 1754, ], 'background_gradient' => null, 'instructions' => [], 'formats' => ['pdf', 'png'], ]; $normalized = array_replace_recursive($defaults, $layout); $formats = $normalized['formats'] ?? ['pdf', 'png']; if (! is_array($formats)) { $formats = [$formats]; } $normalizedFormats = []; foreach ($formats as $format) { $value = strtolower((string) $format); if ($value === 'svg') { $value = 'png'; } if (in_array($value, ['pdf', 'png'], true) && ! in_array($value, $normalizedFormats, true)) { $normalizedFormats[] = $value; } } $normalized['formats'] = $normalizedFormats ?: ['pdf', 'png']; return $normalized; } 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', 'png'], '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', 'png']; return [ 'id' => $layout['id'], 'name' => $layout['name'], 'description' => $layout['description'], 'subtitle' => $layout['subtitle'], 'badge_label' => $layout['badge_label'] ?? null, 'instructions_heading' => $layout['instructions_heading'] ?? null, 'link_heading' => $layout['link_heading'] ?? null, 'cta_label' => $layout['cta_label'] ?? null, 'cta_caption' => $layout['cta_caption'] ?? null, 'instructions' => $layout['instructions'] ?? [], 'preview' => [ 'background' => $layout['background'], 'background_gradient' => $layout['background_gradient'], 'accent' => $layout['accent'], 'text' => $layout['text'], 'qr_size_px' => $layout['qr']['size_px'] ?? null, ], 'formats' => $formats, 'download_urls' => collect($formats) ->mapWithKeys(fn ($format) => [$format => $urlResolver($layout['id'], $format)]) ->all(), ]; }, self::all()); } }