267 lines
10 KiB
PHP
267 lines
10 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 = [
|
||
'modern-poster' => [
|
||
'id' => 'modern-poster',
|
||
'name' => 'Modern Poster',
|
||
'subtitle' => 'Große, auffällige Fläche – perfekt für den Eingangsbereich.',
|
||
'description' => 'Helle Posteroptik mit diagonalem Farbband und deutlicher Call-to-Action.',
|
||
'paper' => 'a4',
|
||
'orientation' => 'portrait',
|
||
'background' => '#F8FAFC',
|
||
'text' => '#0F172A',
|
||
'accent' => '#6366F1',
|
||
'secondary' => '#CBD5F5',
|
||
'badge' => '#0EA5E9',
|
||
'qr' => ['size_px' => 340],
|
||
'svg' => ['width' => 1080, 'height' => 1520],
|
||
'instructions' => [
|
||
'Scanne den Code und tritt dem Event direkt bei.',
|
||
'Speichere deine Lieblingsmomente mit Foto-Uploads.',
|
||
'Merke dir dein Gäste-Pseudonym für Likes und Badges.',
|
||
],
|
||
],
|
||
'elegant-frame' => [
|
||
'id' => 'elegant-frame',
|
||
'name' => 'Elegant Frame',
|
||
'subtitle' => 'Ein ruhiges Layout mit Fokus auf Eleganz.',
|
||
'description' => 'Serifen-Schrift, pastellige Flächen und dezente Rahmen für elegante Anlässe.',
|
||
'paper' => 'a4',
|
||
'orientation' => 'portrait',
|
||
'background' => '#FBF7F2',
|
||
'text' => '#2B1B13',
|
||
'accent' => '#C08457',
|
||
'secondary' => '#E6D5C3',
|
||
'badge' => '#8B5CF6',
|
||
'qr' => ['size_px' => 300],
|
||
'svg' => ['width' => 1080, 'height' => 1520],
|
||
'instructions' => [
|
||
'QR-Code scannen oder Link im Browser eingeben.',
|
||
'Name eingeben, Lieblingssprache auswählen und loslegen.',
|
||
'Zeige diesen Druck am Empfang als Orientierung für Gäste.',
|
||
],
|
||
],
|
||
'bold-gradient' => [
|
||
'id' => 'bold-gradient',
|
||
'name' => 'Bold Gradient',
|
||
'subtitle' => 'Farbverlauf mit starkem Kontrast.',
|
||
'description' => 'Ein kraftvolles Farbstatement mit großem QR-Code – ideal für Partys.',
|
||
'paper' => 'a4',
|
||
'orientation' => 'portrait',
|
||
'background' => '#F97316',
|
||
'background_gradient' => [
|
||
'angle' => 190,
|
||
'stops' => ['#F97316', '#EC4899', '#8B5CF6'],
|
||
],
|
||
'text' => '#FFFFFF',
|
||
'accent' => '#FFFFFF',
|
||
'secondary' => 'rgba(255,255,255,0.72)',
|
||
'badge' => '#1E293B',
|
||
'qr' => ['size_px' => 360],
|
||
'svg' => ['width' => 1080, 'height' => 1520],
|
||
'instructions' => [
|
||
'Sofort scannen – der QR-Code führt direkt zum Event.',
|
||
'Fotos knipsen, Challenges lösen und Likes sammeln.',
|
||
'Teile den Link mit Freund:innen, falls kein Scan möglich ist.',
|
||
],
|
||
],
|
||
'photo-strip' => [
|
||
'id' => 'photo-strip',
|
||
'name' => 'Photo Strip',
|
||
'subtitle' => 'Layout mit Fotostreifen-Anmutung und Checkliste.',
|
||
'description' => 'Horizontale Teilung, Platz für Hinweise und Storytelling.',
|
||
'paper' => 'a4',
|
||
'orientation' => 'portrait',
|
||
'background' => '#FFFFFF',
|
||
'text' => '#111827',
|
||
'accent' => '#0EA5E9',
|
||
'secondary' => '#94A3B8',
|
||
'badge' => '#334155',
|
||
'qr' => ['size_px' => 320],
|
||
'svg' => ['width' => 1080, 'height' => 1520],
|
||
'instructions' => [
|
||
'Schritt 1: QR-Code scannen oder Kurzlink nutzen.',
|
||
'Schritt 2: Profilname eingeben – kreativ sein!',
|
||
'Schritt 3: Fotos hochladen und Teamaufgaben lösen.',
|
||
],
|
||
],
|
||
'minimal-card' => [
|
||
'id' => 'minimal-card',
|
||
'name' => 'Minimal Card',
|
||
'subtitle' => 'Kleine Karte – mehrfach druckbar als Tischaufsteller.',
|
||
'description' => 'Schlichtes Kartenformat mit klarer Typografie und viel Weißraum.',
|
||
'paper' => 'a4',
|
||
'orientation' => 'portrait',
|
||
'background' => '#F9FAFB',
|
||
'text' => '#111827',
|
||
'accent' => '#9333EA',
|
||
'secondary' => '#E0E7FF',
|
||
'badge' => '#64748B',
|
||
'qr' => ['size_px' => 280],
|
||
'svg' => ['width' => 1080, 'height' => 1520],
|
||
'instructions' => [
|
||
'Code scannen, Profil erstellen, Erinnerungen festhalten.',
|
||
'Halte diese Karte an mehreren Stellen bereit.',
|
||
'Für Ausdrucke auf 200 g/m² Kartenpapier empfohlen.',
|
||
],
|
||
],
|
||
];
|
||
|
||
/**
|
||
* 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' => 320,
|
||
],
|
||
'svg' => [
|
||
'width' => 1080,
|
||
'height' => 1520,
|
||
],
|
||
'background_gradient' => null,
|
||
'instructions' => [],
|
||
'formats' => ['pdf', 'svg'],
|
||
];
|
||
|
||
return array_replace_recursive($defaults, $layout);
|
||
}
|
||
|
||
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', 'svg'],
|
||
'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', 'svg'];
|
||
|
||
return [
|
||
'id' => $layout['id'],
|
||
'name' => $layout['name'],
|
||
'description' => $layout['description'],
|
||
'subtitle' => $layout['subtitle'],
|
||
'preview' => [
|
||
'background' => $layout['background'],
|
||
'background_gradient' => $layout['background_gradient'],
|
||
'accent' => $layout['accent'],
|
||
'text' => $layout['text'],
|
||
],
|
||
'formats' => $formats,
|
||
'download_urls' => collect($formats)
|
||
->mapWithKeys(fn ($format) => [$format => $urlResolver($layout['id'], $format)])
|
||
->all(),
|
||
];
|
||
}, self::all());
|
||
}
|
||
}
|