Files
fotospiel-app/app/Support/JoinTokenLayoutRegistry.php
2025-10-31 20:19:09 +01:00

336 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 funktionierts',
'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());
}
}