further layout preview fixes

This commit is contained in:
Codex Agent
2025-12-12 08:34:19 +01:00
parent 57be7d0030
commit 7cf7c4b8df
8 changed files with 1767 additions and 438 deletions

View File

@@ -6,6 +6,30 @@ use App\Models\InviteLayout;
class JoinTokenLayoutRegistry
{
private const DEFAULT_DESCRIPTION = 'Helft uns, diesen besonderen Tag mit euren schönen Momenten festzuhalten.';
private const DEFAULT_INSTRUCTIONS = [
'QR-Code scannen'."\n".'Kamera-App öffnen und auf den Code richten.',
'Webseite öffnen'."\n".'Der Link öffnet direkt das gemeinsame Jubiläumsalbum.',
'Fotos hochladen'."\n".'Zeigt eure Lieblingsmomente oder erfüllt kleine Fotoaufgaben, um besondere Erinnerungen beizusteuern.',
];
private const SLOTS_PORTRAIT = [
'headline' => ['x' => 0.08, 'y' => 0.1, 'w' => 0.84, 'h' => 0.12, 'fontSize' => 30, 'fontWeight' => 800, 'align' => 'center'],
'subtitle' => ['x' => 0.1, 'y' => 0.13, 'w' => 0.8, 'h' => 0.08, 'fontSize' => 18, 'fontWeight' => 600, 'align' => 'center'],
'description' => ['x' => 0.1, 'y' => 0.18, 'w' => 0.8, 'h' => 0.12, 'fontSize' => 16, 'lineHeight' => 1.4, 'align' => 'center'],
'qr' => ['x' => 0.35, 'y' => 0.4, 'w' => 0.3, 'h' => 0.3],
'instructions' => ['x' => 0.12, 'y' => 0.72, 'w' => 0.76, 'h' => 0.16, 'fontSize' => 12, 'lineHeight' => 1.3, 'align' => 'center'],
];
private const SLOTS_FOLDABLE = [
'headline' => ['x' => 0.1, 'y' => 0.1, 'w' => 0.8, 'h' => 0.12, 'fontSize' => 28, 'fontWeight' => 800, 'align' => 'center'],
'subtitle' => ['x' => 0.12, 'y' => 0.18, 'w' => 0.76, 'h' => 0.08, 'fontSize' => 18, 'fontWeight' => 600, 'align' => 'center'],
'description' => ['x' => 0.12, 'y' => 0.24, 'w' => 0.76, 'h' => 0.12, 'fontSize' => 15, 'lineHeight' => 1.4, 'align' => 'center'],
'qr' => ['x' => 0.36, 'y' => 0.4, 'w' => 0.28, 'h' => 0.28],
'instructions' => ['x' => 0.14, 'y' => 0.72, 'w' => 0.72, 'h' => 0.16, 'fontSize' => 12, 'lineHeight' => 1.3, 'align' => 'center'],
];
/**
* Layout definitions for printable invite cards.
*
@@ -16,15 +40,17 @@ class JoinTokenLayoutRegistry
'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'],
'description' => self::DEFAULT_DESCRIPTION,
'paper' => 'a4',
'orientation' => 'landscape',
'panel_mode' => 'double-mirror',
'format_hint' => 'foldable-a5',
'slots' => self::SLOTS_FOLDABLE,
'container_padding_px' => 28,
'background' => '#F8FAFC',
'background_gradient' => [
'angle' => 180,
'stops' => ['#F8FAFC', '#EEF2FF', '#F8FAFC'],
],
'text' => '#0F172A',
'accent' => '#2563EB',
@@ -38,25 +64,22 @@ class JoinTokenLayoutRegistry
'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.',
],
'instructions' => self::DEFAULT_INSTRUCTIONS,
],
'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'],
],
'description' => self::DEFAULT_DESCRIPTION,
'paper' => 'a4',
'orientation' => 'portrait',
'format_hint' => 'poster-a4',
'slots' => self::SLOTS_PORTRAIT,
'background' => '#FBF7F2',
'background_gradient' => [
'angle' => 165,
'stops' => ['#FBF7F2', '#FDECEF', '#F4F0FF'],
],
'text' => '#2C1A27',
'accent' => '#B85C76',
'secondary' => '#E7D6DC',
@@ -69,26 +92,22 @@ class JoinTokenLayoutRegistry
'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.',
],
'instructions' => self::DEFAULT_INSTRUCTIONS,
],
'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'],
],
'description' => self::DEFAULT_DESCRIPTION,
'paper' => 'a4',
'orientation' => 'portrait',
'format_hint' => 'poster-a4',
'slots' => self::SLOTS_PORTRAIT,
'background' => '#0B132B',
'background_gradient' => [
'angle' => 200,
'stops' => ['#0B132B', '#1C2541', '#274690'],
],
'text' => '#F8FAFC',
'accent' => '#F9C74F',
'secondary' => '#4E5D8F',
@@ -101,26 +120,22 @@ class JoinTokenLayoutRegistry
'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.',
],
'instructions' => self::DEFAULT_INSTRUCTIONS,
],
'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'],
],
'description' => self::DEFAULT_DESCRIPTION,
'paper' => 'a4',
'orientation' => 'portrait',
'format_hint' => 'poster-a4',
'slots' => self::SLOTS_PORTRAIT,
'background' => '#F6F9F4',
'background_gradient' => [
'angle' => 120,
'stops' => ['#F6F9F4', '#EEF5E7', '#F8FAF0'],
],
'text' => '#2F4030',
'accent' => '#6BAA75',
'secondary' => '#DDE9D8',
@@ -133,26 +148,22 @@ class JoinTokenLayoutRegistry
'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.',
],
'instructions' => self::DEFAULT_INSTRUCTIONS,
],
'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'],
],
'description' => self::DEFAULT_DESCRIPTION,
'paper' => 'a4',
'orientation' => 'portrait',
'format_hint' => 'poster-a4',
'slots' => self::SLOTS_PORTRAIT,
'background' => '#1B1A44',
'background_gradient' => [
'angle' => 205,
'stops' => ['#1B1A44', '#42275A', '#734B8F'],
],
'text' => '#FDF7FF',
'accent' => '#F9A826',
'secondary' => '#DDB7FF',
@@ -165,26 +176,22 @@ class JoinTokenLayoutRegistry
'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.',
],
'instructions' => self::DEFAULT_INSTRUCTIONS,
],
'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'],
],
'description' => self::DEFAULT_DESCRIPTION,
'paper' => 'a4',
'orientation' => 'portrait',
'format_hint' => 'poster-a4',
'slots' => self::SLOTS_PORTRAIT,
'background' => '#FFF9F0',
'background_gradient' => [
'angle' => 145,
'stops' => ['#FFF9F0', '#FFEFEF', '#FFF5D6'],
],
'text' => '#31291F',
'accent' => '#FF6F61',
'secondary' => '#F9D6A5',
@@ -197,13 +204,7 @@ class JoinTokenLayoutRegistry
'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.',
],
'instructions' => self::DEFAULT_INSTRUCTIONS,
],
];
@@ -280,7 +281,7 @@ class JoinTokenLayoutRegistry
'height' => 1754,
],
'background_gradient' => null,
'instructions' => [],
'instructions' => self::DEFAULT_INSTRUCTIONS,
'formats' => ['pdf', 'png'],
];
@@ -308,11 +309,22 @@ class JoinTokenLayoutRegistry
return $normalized;
}
private static function defaultSlotsPortrait(): array
{
return self::SLOTS_PORTRAIT;
}
private static function defaultSlotsFoldable(): array
{
return self::SLOTS_FOLDABLE;
}
private static function fromModel(InviteLayout $layout): array
{
$preview = $layout->preview ?? [];
$options = $layout->layout_options ?? [];
$instructions = $layout->instructions ?? [];
$slots = $options['slots'] ?? null;
return array_filter([
'id' => $layout->slug,
@@ -321,6 +333,7 @@ class JoinTokenLayoutRegistry
'description' => $layout->description,
'paper' => $layout->paper,
'orientation' => $layout->orientation,
'format_hint' => self::resolveFormatHint($layout->paper, $layout->orientation, $layout->panel_mode),
'background' => $preview['background'] ?? null,
'background_gradient' => $preview['background_gradient'] ?? null,
'text' => $preview['text'] ?? null,
@@ -334,6 +347,7 @@ class JoinTokenLayoutRegistry
'cta_caption' => $options['cta_caption'] ?? null,
'link_label' => $options['link_label'] ?? null,
'logo_url' => $options['logo_url'] ?? null,
'slots' => is_array($slots) ? $slots : null,
'qr' => array_filter([
'size_px' => $preview['qr']['size_px'] ?? $options['qr']['size_px'] ?? $preview['qr_size_px'] ?? $options['qr_size_px'] ?? null,
]),
@@ -346,6 +360,23 @@ class JoinTokenLayoutRegistry
], fn ($value) => $value !== null && $value !== []);
}
private static function resolveFormatHint(?string $paper, ?string $orientation, ?string $panelMode): ?string
{
$paperVal = strtolower((string) $paper);
$orientationVal = strtolower((string) $orientation);
$panelVal = strtolower((string) $panelMode);
if ($paperVal === 'a4' && $orientationVal === 'portrait' && $panelVal !== 'double-mirror') {
return 'poster-a4';
}
if ($paperVal === 'a4' && $orientationVal === 'landscape' && $panelVal === 'double-mirror') {
return 'foldable-a5';
}
return null;
}
/**
* Map layouts into an API-ready response structure, attaching URLs.
*
@@ -365,18 +396,23 @@ class JoinTokenLayoutRegistry
'paper' => $layout['paper'] ?? 'a4',
'orientation' => $layout['orientation'] ?? 'portrait',
'panel_mode' => $layout['panel_mode'] ?? null,
'format_hint' => $layout['format_hint'] ?? self::resolveFormatHint($layout['paper'] ?? null, $layout['orientation'] ?? null, $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'] ?? [],
'slots' => $layout['slots'] ?? null,
'preview' => [
'background' => $layout['background'],
'background_gradient' => $layout['background_gradient'],
'accent' => $layout['accent'],
'text' => $layout['text'],
'qr_size_px' => $layout['qr']['size_px'] ?? null,
'aspect_ratio' => isset($layout['svg']['width'], $layout['svg']['height']) && $layout['svg']['width'] && $layout['svg']['height']
? (float) $layout['svg']['width'] / (float) $layout['svg']['height']
: null,
],
'formats' => $formats,
'download_urls' => collect($formats)