zu fabricjs gewechselt, noch nicht funktionsfähig

This commit is contained in:
Codex Agent
2025-10-31 20:19:09 +01:00
parent 06df61f706
commit eb0c31c90b
33 changed files with 7718 additions and 2062 deletions

View File

@@ -97,11 +97,23 @@ class InviteLayoutResource extends Resource
}
$layoutOptions = $data['layout_options'] ?? [];
$formats = $layoutOptions['formats'] ?? ['pdf', 'svg'];
$formats = $layoutOptions['formats'] ?? ['pdf', 'png'];
if (is_string($formats)) {
$formats = array_values(array_filter(array_map('trim', explode(',', $formats))));
}
$layoutOptions['formats'] = $formats ?: ['pdf', 'svg'];
$normalizedFormats = [];
foreach ($formats ?: ['pdf', 'png'] 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;
}
}
$layoutOptions['formats'] = $normalizedFormats ?: ['pdf', 'png'];
$data['layout_options'] = array_filter([
'badge_label' => $layoutOptions['badge_label'] ?? null,

View File

@@ -11,6 +11,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class EventJoinTokenController extends Controller
{
@@ -140,6 +141,24 @@ class EventJoinTokenController extends Controller
'metadata.layout_customization.background_gradient.angle' => ['nullable', 'numeric'],
'metadata.layout_customization.background_gradient.stops' => ['nullable', 'array', 'max:5'],
'metadata.layout_customization.background_gradient.stops.*' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/'],
'metadata.layout_customization.mode' => ['nullable', Rule::in(['standard', 'advanced'])],
'metadata.layout_customization.elements' => ['nullable', 'array', 'max:50'],
'metadata.layout_customization.elements.*.id' => ['required_with:metadata.layout_customization.elements', 'string', 'max:120'],
'metadata.layout_customization.elements.*.type' => ['required_with:metadata.layout_customization.elements', Rule::in(['qr', 'headline', 'subtitle', 'description', 'link', 'badge', 'logo', 'cta', 'text'])],
'metadata.layout_customization.elements.*.x' => ['nullable', 'numeric', 'min:0'],
'metadata.layout_customization.elements.*.y' => ['nullable', 'numeric', 'min:0'],
'metadata.layout_customization.elements.*.width' => ['nullable', 'numeric', 'min:40'],
'metadata.layout_customization.elements.*.height' => ['nullable', 'numeric', 'min:40'],
'metadata.layout_customization.elements.*.rotation' => ['nullable', 'numeric'],
'metadata.layout_customization.elements.*.font_size' => ['nullable', 'numeric', 'min:8', 'max:160'],
'metadata.layout_customization.elements.*.align' => ['nullable', Rule::in(['left', 'center', 'right'])],
'metadata.layout_customization.elements.*.content' => ['nullable', 'string', 'max:400'],
'metadata.layout_customization.elements.*.font_family' => ['nullable', 'string', 'max:120'],
'metadata.layout_customization.elements.*.letter_spacing' => ['nullable', 'numeric', 'min:-5', 'max:20'],
'metadata.layout_customization.elements.*.line_height' => ['nullable', 'numeric', 'min:0.5', 'max:3'],
'metadata.layout_customization.elements.*.fill' => ['nullable', 'string', 'max:20'],
'metadata.layout_customization.elements.*.locked' => ['nullable', 'boolean'],
'metadata.layout_customization.elements.*.initial' => ['nullable', 'boolean'],
];
$validated = $request->validate($rules);
@@ -156,6 +175,36 @@ class EventJoinTokenController extends Controller
unset($validated['metadata']['layout_customization']['logo_data_url']);
}
if (isset($validated['metadata']['layout_customization']['elements'])
&& is_array($validated['metadata']['layout_customization']['elements'])) {
$validated['metadata']['layout_customization']['elements'] = array_values(array_filter(array_map(
static function ($element) {
if (! is_array($element) || empty($element['id']) || empty($element['type'])) {
return null;
}
return array_filter([
'id' => (string) $element['id'],
'type' => (string) $element['type'],
'x' => array_key_exists('x', $element) ? (float) $element['x'] : null,
'y' => array_key_exists('y', $element) ? (float) $element['y'] : null,
'width' => array_key_exists('width', $element) ? (float) $element['width'] : null,
'height' => array_key_exists('height', $element) ? (float) $element['height'] : null,
'rotation' => array_key_exists('rotation', $element) ? (float) $element['rotation'] : null,
'font_size' => array_key_exists('font_size', $element) ? (float) $element['font_size'] : null,
'align' => $element['align'] ?? null,
'content' => array_key_exists('content', $element) ? (string) $element['content'] : null,
'font_family' => $element['font_family'] ?? null,
'letter_spacing' => array_key_exists('letter_spacing', $element) ? (float) $element['letter_spacing'] : null,
'line_height' => array_key_exists('line_height', $element) ? (float) $element['line_height'] : null,
'fill' => $element['fill'] ?? null,
'locked' => array_key_exists('locked', $element) ? (bool) $element['locked'] : null,
], static fn ($value) => $value !== null && $value !== '');
},
$validated['metadata']['layout_customization']['elements']
)));
}
return $validated;
}
}

View File

@@ -42,7 +42,7 @@ class EventJoinTokenLayoutController extends Controller
abort(404, 'Layout nicht gefunden.');
}
if (! in_array($format, ['pdf', 'svg'], true)) {
if (! in_array($format, ['pdf', 'png'], true)) {
abort(404, 'Unbekanntes Exportformat.');
}
@@ -69,18 +69,15 @@ class EventJoinTokenLayoutController extends Controller
'qrPngDataUri' => $qrPngDataUri,
'backgroundStyle' => $backgroundStyle,
'customization' => $joinToken->metadata['layout_customization'] ?? null,
'advancedLayout' => $this->buildAdvancedLayout(
$layoutConfig,
$joinToken->metadata['layout_customization'] ?? null,
$qrPngDataUri,
$tokenUrl,
$eventName
),
];
$filename = sprintf('%s-%s.%s', Str::slug($eventName ?: 'event'), $layoutConfig['id'], $format);
if ($format === 'svg') {
$svg = view('layouts.join-token.svg', $viewData)->render();
return response($svg)
->header('Content-Type', 'image/svg+xml')
->header('Content-Disposition', 'attachment; filename="'.$filename.'"');
}
$html = view('layouts.join-token.pdf', $viewData)->render();
$options = new Options;
@@ -93,9 +90,46 @@ class EventJoinTokenLayoutController extends Controller
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->render();
return response($dompdf->output())
$pdfBinary = $dompdf->output();
$filenameStem = sprintf('%s-%s', Str::slug($eventName ?: 'event'), $layoutConfig['id']);
if ($format === 'png') {
if (! class_exists(\Imagick::class)) {
abort(500, 'PNG-Export erfordert Imagick.');
}
try {
$imagick = new \Imagick;
$imagick->setResolution(300, 300);
$imagick->setBackgroundColor(new \ImagickPixel('white'));
$imagick->setOption('pdf:use-cropbox', 'true');
$imagick->readImageBlob($pdfBinary);
$imagick->setIteratorIndex(0);
$flattened = $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
$flattened->setImageFormat('png');
$flattened->setImageUnits(\Imagick::RESOLUTION_PIXELSPERINCH);
$flattened->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
$pngBinary = $flattened->getImagesBlob();
$flattened->clear();
$flattened->destroy();
$imagick->clear();
$imagick->destroy();
} catch (\Throwable $exception) {
report($exception);
abort(500, 'PNG-Export konnte nicht erzeugt werden.');
}
return response($pngBinary)
->header('Content-Type', 'image/png')
->header('Content-Disposition', 'attachment; filename="'.$filenameStem.'.png"');
}
return response($pdfBinary)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="'.$filename.'"');
->header('Content-Disposition', 'attachment; filename="'.$filenameStem.'.pdf"');
}
private function ensureBelongsToEvent(Event $event, EventJoinToken $joinToken): void
@@ -182,4 +216,168 @@ class EventJoinTokenLayoutController extends Controller
return $layout['background'] ?? '#FFFFFF';
}
private function buildAdvancedLayout(array $layout, $customization, string $qrPngDataUri, string $tokenUrl, string $eventName): ?array
{
if (! is_array($customization)) {
return null;
}
if (($customization['mode'] ?? null) !== 'advanced') {
return null;
}
$elements = $customization['elements'] ?? null;
if (! is_array($elements) || empty($elements)) {
return null;
}
$width = (int) ($layout['svg']['width'] ?? 1080);
$height = (int) ($layout['svg']['height'] ?? 1520);
$accent = $layout['accent'] ?? '#6366F1';
$text = $layout['text'] ?? '#0F172A';
$secondary = $layout['secondary'] ?? '#1F2937';
$badge = $layout['badge'] ?? $accent;
$resolved = [];
foreach ($elements as $element) {
if (! is_array($element) || empty($element['id']) || empty($element['type'])) {
continue;
}
$type = (string) $element['type'];
$dimensions = $this->normalizeElementDimensions($type, $element, $width, $height);
$content = $this->resolveElementContent($type, $customization, $layout, $eventName, $tokenUrl, $element['content'] ?? null);
$style = [
'position' => 'absolute',
'left' => sprintf('%dpx', $dimensions['x']),
'top' => sprintf('%dpx', $dimensions['y']),
'width' => sprintf('%dpx', $dimensions['width']),
'height' => sprintf('%dpx', $dimensions['height']),
];
if (isset($element['rotation']) && is_numeric($element['rotation']) && (float) $element['rotation'] !== 0.0) {
$style['transform'] = sprintf('rotate(%sdeg)', (float) $element['rotation']);
$style['transform-origin'] = 'center center';
}
$fontSize = isset($element['font_size']) && is_numeric($element['font_size'])
? max(8, min(160, (float) $element['font_size']))
: null;
$lineHeight = isset($element['line_height']) && is_numeric($element['line_height'])
? max(0.5, min(3.0, (float) $element['line_height']))
: null;
$letterSpacing = isset($element['letter_spacing']) && is_numeric($element['letter_spacing'])
? (float) $element['letter_spacing']
: null;
$resolved[] = [
'id' => (string) $element['id'],
'type' => $type,
'content' => $content,
'align' => $element['align'] ?? null,
'font_size' => $fontSize,
'line_height' => $lineHeight,
'letter_spacing' => $letterSpacing,
'font_family' => $element['font_family'] ?? null,
'fill' => $element['fill'] ?? null,
'style' => $style,
'style_string' => $this->styleToString($style),
'width' => $dimensions['width'],
'height' => $dimensions['height'],
'asset' => match ($type) {
'qr' => $qrPngDataUri,
'logo' => $customization['logo_data_url'] ?? $customization['logo_url'] ?? $layout['logo_url'] ?? null,
default => null,
},
];
}
if (empty($resolved)) {
return null;
}
return [
'width' => $width,
'height' => $height,
'background' => $layout['background'] ?? '#FFFFFF',
'background_gradient' => $layout['background_gradient'] ?? null,
'accent' => $accent,
'text' => $text,
'secondary' => $secondary,
'badge' => $badge,
'qr_src' => $qrPngDataUri,
'logo_src' => $customization['logo_data_url'] ?? $customization['logo_url'] ?? $layout['logo_url'] ?? null,
'elements' => $resolved,
];
}
private function resolveElementContent(string $type, array $customization, array $layout, string $eventName, string $tokenUrl, $fallback = null): ?string
{
return match ($type) {
'headline' => $customization['headline'] ?? $eventName,
'subtitle' => $customization['subtitle'] ?? ($layout['subtitle'] ?? ''),
'description' => $customization['description'] ?? ($layout['description'] ?? ''),
'link' => (isset($customization['link_label']) && trim($customization['link_label']) !== '')
? $customization['link_label']
: $tokenUrl,
'badge' => $customization['badge_label'] ?? ($layout['badge_label'] ?? 'Digitale Gästebox'),
'cta' => $customization['cta_label'] ?? ($layout['cta_label'] ?? 'Scan mich & starte direkt'),
default => $fallback !== null ? (string) $fallback : null,
};
}
private function styleToString(array $style): string
{
return implode(';', array_map(
static fn ($key, $value) => sprintf('%s:%s', $key, $value),
array_keys($style),
$style
));
}
private function normalizeElementDimensions(string $type, array $element, int $canvasWidth, int $canvasHeight): array
{
$minDimensions = match ($type) {
'qr' => ['width' => 240, 'height' => 240],
'logo' => ['width' => 140, 'height' => 100],
'badge' => ['width' => 160, 'height' => 60],
'cta' => ['width' => 200, 'height' => 80],
'text' => ['width' => 200, 'height' => 120],
default => ['width' => 160, 'height' => 80],
};
$width = (int) round((float) ($element['width'] ?? $minDimensions['width']));
$height = (int) round((float) ($element['height'] ?? $minDimensions['height']));
$width = max($minDimensions['width'], min($width, $canvasWidth));
$height = max($minDimensions['height'], min($height, $canvasHeight));
if ($type === 'qr') {
$size = (int) round(max($width, $height));
$size = max(240, min($size, min($canvasWidth, $canvasHeight)));
$width = $height = $size;
}
$maxX = max($canvasWidth - $width, 0);
$maxY = max($canvasHeight - $height, 0);
$x = (int) round((float) ($element['x'] ?? 0));
$y = (int) round((float) ($element['y'] ?? 0));
$x = max(0, min($x, $maxX));
$y = max(0, min($y, $maxY));
return [
'x' => $x,
'y' => $y,
'width' => $width,
'height' => $height,
];
}
}

View File

@@ -46,16 +46,16 @@ class EventJoinTokenResource extends JsonResource
if ($qrCodeUrl) {
try {
$svg = QrCode::format('svg')
$png = QrCode::format('png')
->size(360)
->margin(1)
->errorCorrection('M')
->generate($qrCodeUrl);
$svgString = (string) $svg;
$pngBinary = (string) $png;
if ($svgString !== '') {
$qrCodeDataUrl = 'data:image/svg+xml;base64,'.base64_encode($svgString);
if ($pngBinary !== '') {
$qrCodeDataUrl = 'data:image/png;base64,'.base64_encode($pngBinary);
}
} catch (\Throwable $exception) {
report($exception);

View File

@@ -12,108 +12,149 @@ class JoinTokenLayoutRegistry
* @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' => 500],
'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.',
'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',
'text' => '#2B1B13',
'accent' => '#C08457',
'secondary' => '#E6D5C3',
'badge' => '#8B5CF6',
'qr' => ['size_px' => 460],
'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'],
'angle' => 165,
'stops' => ['#FBF7F2', '#FDECEF', '#F4F0FF'],
],
'text' => '#FFFFFF',
'accent' => '#FFFFFF',
'secondary' => 'rgba(255,255,255,0.72)',
'badge' => '#1E293B',
'qr' => ['size_px' => 540],
'svg' => ['width' => 1080, 'height' => 1520],
'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' => [
'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.',
'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.',
],
],
'photo-strip' => [
'id' => 'photo-strip',
'name' => 'Photo Strip',
'subtitle' => 'Layout mit Fotostreifen-Anmutung und Checkliste.',
'description' => 'Horizontale Teilung, Platz für Hinweise und Storytelling.',
'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' => '#FFFFFF',
'text' => '#111827',
'accent' => '#0EA5E9',
'secondary' => '#94A3B8',
'badge' => '#334155',
'qr' => ['size_px' => 500],
'svg' => ['width' => 1080, 'height' => 1520],
'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' => [
'Schritt 1: QR-Code scannen oder Kurzlink nutzen.',
'Schritt 2: Profilname eingeben kreativ sein!',
'Schritt 3: Fotos hochladen und Teamaufgaben lösen.',
'QR-Code scannen oder Kurzlink eingeben.',
'Mit Firmen-E-Mail anmelden und Zugang bestätigen.',
'Agenda verfolgen, Fotos teilen und Highlights voten.',
],
],
'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.',
'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' => '#F9FAFB',
'text' => '#111827',
'accent' => '#9333EA',
'secondary' => '#E0E7FF',
'badge' => '#64748B',
'qr' => ['size_px' => 440],
'svg' => ['width' => 1080, 'height' => 1520],
'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' => [
'Code scannen, Profil erstellen, Erinnerungen festhalten.',
'Halte diese Karte an mehreren Stellen bereit.',
'Für Ausdrucke auf 200g/m² Kartenpapier empfohlen.',
'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!',
],
],
];
@@ -182,18 +223,39 @@ class JoinTokenLayoutRegistry
'link_label' => null,
'logo_url' => null,
'qr' => [
'size_px' => 320,
'size_px' => 360,
],
'svg' => [
'width' => 1080,
'height' => 1520,
'width' => 1240,
'height' => 1754,
],
'background_gradient' => null,
'instructions' => [],
'formats' => ['pdf', 'svg'],
'formats' => ['pdf', 'png'],
];
return array_replace_recursive($defaults, $layout);
$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
@@ -229,7 +291,7 @@ class JoinTokenLayoutRegistry
'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'],
'formats' => $options['formats'] ?? ['pdf', 'png'],
'instructions' => $instructions,
], fn ($value) => $value !== null && $value !== []);
}
@@ -243,13 +305,19 @@ class JoinTokenLayoutRegistry
public static function toResponse(callable $urlResolver): array
{
return array_map(function (array $layout) use ($urlResolver) {
$formats = $layout['formats'] ?? ['pdf', 'svg'];
$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'],