336 lines
13 KiB
PHP
336 lines
13 KiB
PHP
<?php
|
||
|
||
namespace App\Support;
|
||
|
||
use App\Models\InviteLayout;
|
||
|
||
class JoinTokenLayoutRegistry
|
||
{
|
||
/**
|
||
* Layout definitions for printable invite cards.
|
||
*
|
||
* @var array<string, array>
|
||
*/
|
||
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' => 520],
|
||
'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' => 560],
|
||
'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' => 520],
|
||
'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' => 560],
|
||
'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' => 520],
|
||
'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<int, array<string, mixed>>
|
||
*/
|
||
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' => 360,
|
||
],
|
||
'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<int, array<string, mixed>>
|
||
*/
|
||
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());
|
||
}
|
||
}
|