Files
fotospiel-app/app/Support/JoinTokenLayoutRegistry.php

389 lines
16 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 = [
'foldable-table-a5' => [
'id' => 'foldable-table-a5',
'name' => 'Foldable Table Card (A5)',
'subtitle' => 'Doppelseitige Tischkarte zum Falten QR vorn & hinten.',
'description' => 'Zwei identische Hälften auf A4 quer, rechte Seite gespiegelt für sauberes Falten.',
'paper' => 'a4',
'orientation' => 'landscape',
'panel_mode' => 'double-mirror',
'container_padding_px' => 28,
'background' => '#F8FAFC',
'background_gradient' => [
'angle' => 180,
'stops' => ['#F8FAFC', '#EEF2FF', '#F8FAFC'],
],
'text' => '#0F172A',
'accent' => '#2563EB',
'secondary' => '#E0E7FF',
'badge' => '#1D4ED8',
'badge_label' => 'Digitale Gästebox',
'instructions_heading' => "So funktioniert's",
'link_heading' => 'Alternative zum Einscannen',
'cta_label' => 'Scan & loslegen',
'cta_caption' => 'Kein Login nötig',
'link_label' => 'fotospiel.app/DEINCODE',
'qr' => ['size_px' => 520],
'svg' => ['width' => 1754, 'height' => 1240],
'instructions' => [
'QR-Code scannen oder Kurzlink öffnen.',
'Anzeigenamen wählen kein Account nötig.',
'Fotos hochladen, liken & kommentieren.',
'Challenges spielen und Punkte sammeln.',
],
],
'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' => 'Digitale Gästebox',
'instructions_heading' => 'So läuft\'s für eure Gäste',
'link_heading' => 'Kein Scanner? Einfach Kurzlink öffnen',
'link_label' => 'fotospiel.app/DEINCODE',
'cta_label' => 'Fotos & Grüße teilen',
'cta_caption' => 'Sofort starten',
'qr' => ['size_px' => 640],
'svg' => ['width' => 1240, 'height' => 1754],
'instructions' => [
'QR-Code scannen oder fotospiel.app/DEINCODE eingeben.',
'Anzeigenamen wählen kein Account nötig.',
'Fotos hochladen und Aufgaben erfüllen, so oft ihr wollt.',
'Highlights liken, Kommentare und Grüße dalassen.',
'Datenschutz ready: anonyme Sessions, keine App-Installation.',
],
],
'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' => 'Digitale Gästebox',
'instructions_heading' => 'So läuft\'s für eure Gäste',
'link_heading' => 'Kein Scanner? Einfach Kurzlink öffnen',
'link_label' => 'fotospiel.app/DEINCODE',
'cta_label' => 'Scan & losknipsen',
'cta_caption' => 'Keine App nötig',
'qr' => ['size_px' => 640],
'svg' => ['width' => 1240, 'height' => 1754],
'instructions' => [
'QR-Code scannen oder fotospiel.app/DEINCODE eingeben.',
'Anzeigenamen wählen kein Account nötig.',
'Fotos hochladen und Aufgaben erfüllen, so oft ihr wollt.',
'Highlights liken, Kommentare und Grüße dalassen.',
'Datenschutz ready: anonyme Sessions, keine App-Installation.',
],
],
'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' => 'Digitale Gästebox',
'instructions_heading' => 'So läuft\'s für eure Gäste',
'link_heading' => 'Kein Scanner? Einfach Kurzlink öffnen',
'link_label' => 'fotospiel.app/DEINCODE',
'cta_label' => 'Jetzt Erinnerungen hochladen',
'cta_caption' => 'Los gehts',
'qr' => ['size_px' => 660],
'svg' => ['width' => 1240, 'height' => 1754],
'instructions' => [
'QR-Code scannen oder fotospiel.app/DEINCODE eingeben.',
'Anzeigenamen wählen kein Account nötig.',
'Fotos hochladen und Aufgaben erfüllen, so oft ihr wollt.',
'Highlights liken, Kommentare und Grüße dalassen.',
'Datenschutz ready: anonyme Sessions, keine App-Installation.',
],
],
'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' => 'Digitale Gästebox',
'instructions_heading' => 'So läuft\'s für eure Gäste',
'link_heading' => 'Kein Scanner? Einfach Kurzlink öffnen',
'link_label' => 'fotospiel.app/DEINCODE',
'cta_label' => 'Galerie öffnen',
'cta_caption' => 'Challenges spielen',
'qr' => ['size_px' => 680],
'svg' => ['width' => 1240, 'height' => 1754],
'instructions' => [
'QR-Code scannen oder fotospiel.app/DEINCODE eingeben.',
'Anzeigenamen wählen kein Account nötig.',
'Fotos hochladen und Aufgaben erfüllen, so oft ihr wollt.',
'Highlights liken, Kommentare und Grüße dalassen.',
'Datenschutz ready: anonyme Sessions, keine App-Installation.',
],
],
'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' => 'Digitale Gästebox',
'instructions_heading' => 'So läuft\'s für eure Gäste',
'link_heading' => 'Kein Scanner? Einfach Kurzlink öffnen',
'link_label' => 'fotospiel.app/DEINCODE',
'cta_label' => 'Uploads beginnen',
'cta_caption' => 'Likes vergeben',
'qr' => ['size_px' => 680],
'svg' => ['width' => 1240, 'height' => 1754],
'instructions' => [
'QR-Code scannen oder fotospiel.app/DEINCODE eingeben.',
'Anzeigenamen wählen kein Account nötig.',
'Fotos hochladen und Aufgaben erfüllen, so oft ihr wollt.',
'Highlights liken, Kommentare und Grüße dalassen.',
'Datenschutz ready: anonyme Sessions, keine App-Installation.',
],
],
];
/**
* 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',
'panel_mode' => null,
'container_padding_px' => 48,
'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<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'],
'paper' => $layout['paper'] ?? 'a4',
'orientation' => $layout['orientation'] ?? 'portrait',
'panel_mode' => $layout['panel_mode'] ?? null,
'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());
}
}