- Reworked the tenant admin login page

- Updated the User model to implement Filament’s tenancy contracts
- Seeded a ready-to-use demo tenant (user, tenant, active package, purchase)
- Introduced a branded, translated 403 error page to replace the generic forbidden message for unauthorised admin hits
- Removed the public “Register” links from the marketing header
- hardened join event logic and improved error handling in the guest pwa.
This commit is contained in:
Codex Agent
2025-10-13 12:50:46 +02:00
parent 9394c3171e
commit 64a5411fb9
69 changed files with 5447 additions and 588 deletions

View File

@@ -0,0 +1,197 @@
<?php
namespace App\Support;
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 200g/m² Kartenpapier empfohlen.',
],
],
];
/**
* Get layout definitions.
*
* @return array<int, array<string, mixed>>
*/
public static function all(): array
{
return array_values(array_map(fn ($layout) => self::normalize($layout), self::LAYOUTS));
}
/**
* Find a layout definition.
*/
public static function find(string $id): ?array
{
$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',
'qr' => [
'size_px' => 320,
],
'svg' => [
'width' => 1080,
'height' => 1520,
],
'background_gradient' => null,
'instructions' => [],
];
return array_replace_recursive($defaults, $layout);
}
/**
* 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 = ['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());
}
}