From eb0c31c90b8912423ae977b6fce04d970ade50af Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 31 Oct 2025 20:19:09 +0100 Subject: [PATCH] =?UTF-8?q?zu=20fabricjs=20gewechselt,=20noch=20nicht=20fu?= =?UTF-8?q?nktionsf=C3=A4hig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InviteLayouts/InviteLayoutResource.php | 16 +- .../Api/Tenant/EventJoinTokenController.php | 49 + .../Tenant/EventJoinTokenLayoutController.php | 224 +- .../Tenant/EventJoinTokenResource.php | 8 +- app/Support/JoinTokenLayoutRegistry.php | 254 +- ..._add_missing_source_collection_columns.php | 65 + database/seeders/InviteLayoutSeeder.php | 4 +- database/seeders/TaskCollectionsSeeder.php | 729 ++++-- package-lock.json | 2159 ++++++++++++++++- package.json | 10 +- public/hot.bak | 1 + resources/js/admin/api.ts | 14 +- .../js/admin/components/DevTenantSwitcher.tsx | 53 +- resources/js/admin/dev-tools.ts | 112 +- resources/js/admin/i18n/locales/de/auth.json | 15 +- .../js/admin/i18n/locales/de/management.json | 166 +- resources/js/admin/i18n/locales/en/auth.json | 17 +- .../js/admin/i18n/locales/en/management.json | 166 +- resources/js/admin/pages/EventDetailPage.tsx | 865 +++++-- resources/js/admin/pages/EventFormPage.tsx | 46 +- resources/js/admin/pages/EventInvitesPage.tsx | 728 ++++-- resources/js/admin/pages/EventToolkitPage.tsx | 566 +---- resources/js/admin/pages/EventsPage.tsx | 1 - resources/js/admin/pages/LoginPage.tsx | 141 +- .../InviteLayoutCustomizerPanel.tsx | 1754 ++++++++----- .../invite-layout/DesignerCanvas.tsx | 719 ++++++ .../components/invite-layout/export-utils.ts | 133 + .../pages/components/invite-layout/schema.ts | 530 ++++ .../views/layouts/join-token/pdf.blade.php | 113 +- .../views/layouts/join-token/svg.blade.php | 2 +- routes/api.php | 2 +- .../Tenant/EventInviteAdvancedLayoutTest.php | 116 + .../Feature/Tenant/EventInviteQrCodeTest.php | 2 +- 33 files changed, 7718 insertions(+), 2062 deletions(-) create mode 100644 database/migrations/2025_10_30_095900_add_missing_source_collection_columns.php create mode 100644 public/hot.bak create mode 100644 resources/js/admin/pages/components/invite-layout/DesignerCanvas.tsx create mode 100644 resources/js/admin/pages/components/invite-layout/export-utils.ts create mode 100644 resources/js/admin/pages/components/invite-layout/schema.ts create mode 100644 tests/Feature/Tenant/EventInviteAdvancedLayoutTest.php diff --git a/app/Filament/Resources/InviteLayouts/InviteLayoutResource.php b/app/Filament/Resources/InviteLayouts/InviteLayoutResource.php index 6cb4e8e..e87f978 100644 --- a/app/Filament/Resources/InviteLayouts/InviteLayoutResource.php +++ b/app/Filament/Resources/InviteLayouts/InviteLayoutResource.php @@ -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, diff --git a/app/Http/Controllers/Api/Tenant/EventJoinTokenController.php b/app/Http/Controllers/Api/Tenant/EventJoinTokenController.php index 7765711..93a04cf 100644 --- a/app/Http/Controllers/Api/Tenant/EventJoinTokenController.php +++ b/app/Http/Controllers/Api/Tenant/EventJoinTokenController.php @@ -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; } } diff --git a/app/Http/Controllers/Api/Tenant/EventJoinTokenLayoutController.php b/app/Http/Controllers/Api/Tenant/EventJoinTokenLayoutController.php index 5f2457b..26d76f5 100644 --- a/app/Http/Controllers/Api/Tenant/EventJoinTokenLayoutController.php +++ b/app/Http/Controllers/Api/Tenant/EventJoinTokenLayoutController.php @@ -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, + ]; + } } diff --git a/app/Http/Resources/Tenant/EventJoinTokenResource.php b/app/Http/Resources/Tenant/EventJoinTokenResource.php index 3c8f921..ddd89cb 100644 --- a/app/Http/Resources/Tenant/EventJoinTokenResource.php +++ b/app/Http/Resources/Tenant/EventJoinTokenResource.php @@ -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); diff --git a/app/Support/JoinTokenLayoutRegistry.php b/app/Support/JoinTokenLayoutRegistry.php index 8f87a32..00e1949 100644 --- a/app/Support/JoinTokenLayoutRegistry.php +++ b/app/Support/JoinTokenLayoutRegistry.php @@ -12,108 +12,149 @@ class JoinTokenLayoutRegistry * @var 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 funktioniert’s', + '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 200 g/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'], diff --git a/database/migrations/2025_10_30_095900_add_missing_source_collection_columns.php b/database/migrations/2025_10_30_095900_add_missing_source_collection_columns.php new file mode 100644 index 0000000..b80f4b1 --- /dev/null +++ b/database/migrations/2025_10_30_095900_add_missing_source_collection_columns.php @@ -0,0 +1,65 @@ +foreignId('source_collection_id') + ->nullable() + ->after('event_type_id') + ->constrained('task_collections') + ->nullOnDelete(); + }); + } + + if (Schema::hasTable('tasks')) { + Schema::table('tasks', function (Blueprint $table) { + if (! Schema::hasColumn('tasks', 'source_collection_id')) { + $table->foreignId('source_collection_id') + ->nullable() + ->after('collection_id') + ->constrained('task_collections') + ->nullOnDelete(); + } + + if (! Schema::hasColumn('tasks', 'source_task_id')) { + $table->foreignId('source_task_id') + ->nullable() + ->after('tenant_id') + ->constrained('tasks') + ->nullOnDelete(); + } + }); + } + } + + public function down(): void + { + if (Schema::hasTable('tasks')) { + Schema::table('tasks', function (Blueprint $table) { + if (Schema::hasColumn('tasks', 'source_collection_id')) { + $table->dropForeign(['source_collection_id']); + $table->dropColumn('source_collection_id'); + } + + if (Schema::hasColumn('tasks', 'source_task_id')) { + $table->dropForeign(['source_task_id']); + $table->dropColumn('source_task_id'); + } + }); + } + + if (Schema::hasTable('task_collections') && Schema::hasColumn('task_collections', 'source_collection_id')) { + Schema::table('task_collections', function (Blueprint $table) { + $table->dropForeign(['source_collection_id']); + $table->dropColumn('source_collection_id'); + }); + } + } +}; diff --git a/database/seeders/InviteLayoutSeeder.php b/database/seeders/InviteLayoutSeeder.php index 984ac00..a1295e0 100644 --- a/database/seeders/InviteLayoutSeeder.php +++ b/database/seeders/InviteLayoutSeeder.php @@ -24,7 +24,7 @@ class InviteLayoutSeeder extends Seeder 'text' => $layout['text'] ?? null, 'badge' => $layout['badge'] ?? null, 'qr' => $layout['qr'] ?? ['size_px' => 500], - 'svg' => $layout['svg'] ?? ['width' => 1080, 'height' => 1520], + 'svg' => $layout['svg'] ?? ['width' => 1240, 'height' => 1754], ]; $options = [ @@ -35,7 +35,7 @@ class InviteLayoutSeeder extends Seeder 'cta_caption' => $layout['cta_caption'] ?? 'Scan mich & starte direkt', 'link_label' => $layout['link_label'] ?? null, 'logo_url' => $layout['logo_url'] ?? null, - 'formats' => $layout['formats'] ?? ['pdf', 'svg'], + 'formats' => $layout['formats'] ?? ['pdf', 'png'], ]; InviteLayout::updateOrCreate( diff --git a/database/seeders/TaskCollectionsSeeder.php b/database/seeders/TaskCollectionsSeeder.php index 686cb99..c263a4a 100644 --- a/database/seeders/TaskCollectionsSeeder.php +++ b/database/seeders/TaskCollectionsSeeder.php @@ -7,250 +7,546 @@ use App\Models\EventType; use App\Models\Task; use App\Models\TaskCollection; use Illuminate\Database\Seeder; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; class TaskCollectionsSeeder extends Seeder { + private const MIN_TASKS_PER_EVENT_TYPE = 50; + public function run(): void { - $collections = [ - [ - 'slug' => 'wedding-classics', - 'event_type' => [ - 'slug' => 'wedding', - 'name' => [ - 'de' => 'Hochzeit', - 'en' => 'Wedding', - ], - 'icon' => 'lucide-heart', - ], - 'name' => [ - 'de' => 'Hochzeitsklassiker', - 'en' => 'Wedding Classics', - ], - 'description' => [ - 'de' => 'Kuratierte Aufgaben rund um Trauung, Emotionen und besondere Momente.', - 'en' => 'Curated prompts for vows, emotions, and memorable wedding highlights.', - ], - 'is_default' => true, - 'position' => 10, - 'tasks' => [ - [ - 'slug' => 'wedding-first-look', - 'title' => [ - 'de' => 'Erster Blick des Brautpaares festhalten', - 'en' => 'Capture the couple’s first look', - ], - 'description' => [ - 'de' => 'Halte den Moment fest, in dem sich Braut und Bräutigam zum ersten Mal sehen.', - 'en' => 'Capture the moment when the bride and groom see each other for the first time.', - ], - 'example' => [ - 'de' => 'Fotografiere die Reaktionen aus verschiedenen Blickwinkeln.', - 'en' => 'Photograph their reactions from different angles.', - ], - 'emotion' => [ - 'name' => [ - 'de' => 'Romantik', - 'en' => 'Romance', - ], - 'icon' => 'lucide-heart', - 'color' => '#ec4899', - 'sort_order' => 10, - ], - 'difficulty' => 'easy', - 'sort_order' => 10, - ], - [ - 'slug' => 'wedding-family-hug', - 'title' => [ - 'de' => 'Familienumarmung organisieren', - 'en' => 'Organise a family group hug', - ], - 'description' => [ - 'de' => 'Bitte die wichtigsten Menschen, das Paar gleichzeitig zu umarmen.', - 'en' => 'Ask the closest friends and family to hug the couple at the same time.', - ], - 'example' => [ - 'de' => 'Kombiniere die Umarmung mit einem Toast.', - 'en' => 'Combine the hug with a heartfelt toast.', - ], - 'emotion' => [ - 'name' => [ - 'de' => 'Freude', - 'en' => 'Joy', - ], - 'icon' => 'lucide-smile', - 'color' => '#f59e0b', - 'sort_order' => 20, - ], - 'difficulty' => 'medium', - 'sort_order' => 20, - ], - [ - 'slug' => 'wedding-midnight-sparkler', - 'title' => [ - 'de' => 'Mitternachtsfunkeln mit Wunderkerzen', - 'en' => 'Midnight sparkler moment', - ], - 'description' => [ - 'de' => 'Verteile Wunderkerzen und schafft ein leuchtendes Spalier für das Paar.', - 'en' => 'Hand out sparklers and form a glowing aisle for the couple.', - ], - 'example' => [ - 'de' => 'Koordiniere die Musik und kündige den Countdown an.', - 'en' => 'Coordinate music and announce a countdown.', - ], - 'emotion' => [ - 'name' => [ - 'de' => 'Ekstase', - 'en' => 'Euphoria', - ], - 'icon' => 'lucide-stars', - 'color' => '#6366f1', - 'sort_order' => 30, - ], - 'difficulty' => 'medium', - 'sort_order' => 30, - ], - ], - ], - [ - 'slug' => 'birthday-celebration', - 'event_type' => [ - 'slug' => 'birthday', - 'name' => [ - 'de' => 'Geburtstag', - 'en' => 'Birthday', - ], - 'icon' => 'lucide-cake', - ], - 'name' => [ - 'de' => 'Geburtstags-Highlights', - 'en' => 'Birthday Highlights', - ], - 'description' => [ - 'de' => 'Aufgaben für Überraschungen, Gratulationen und gemeinsames Feiern.', - 'en' => 'Prompts covering surprises, wishes, and shared celebrations.', - ], - 'is_default' => false, - 'position' => 20, - 'tasks' => [ - [ - 'slug' => 'birthday-surprise-wall', - 'title' => [ - 'de' => 'Überraschungswand mit Polaroids gestalten', - 'en' => 'Create a surprise wall filled with instant photos', - ], - 'description' => [ - 'de' => 'Sammle Schnappschüsse der Gäste und befestige sie als Fotowand.', - 'en' => 'Collect snapshots from guests and mount them on a photo wall.', - ], - 'example' => [ - 'de' => 'Schreibe zu jedem Bild einen kurzen Gruß.', - 'en' => 'Add a short message to each picture.', - ], - 'emotion' => [ - 'name' => [ - 'de' => 'Nostalgie', - 'en' => 'Nostalgia', - ], - 'icon' => 'lucide-images', - 'color' => '#f97316', - 'sort_order' => 40, - ], - 'difficulty' => 'easy', - 'sort_order' => 10, - ], - [ - 'slug' => 'birthday-toast-circle', - 'title' => [ - 'de' => 'Gratulationskreis mit kurzen Toasts', - 'en' => 'Circle of toasts', - ], - 'description' => [ - 'de' => 'Bildet einen Kreis und bittet jede Person um einen 10-Sekunden-Toast.', - 'en' => 'Form a circle and ask everyone for a 10-second toast.', - ], - 'example' => [ - 'de' => 'Nimm die Reaktionen als Video auf.', - 'en' => 'Record the reactions on video.', - ], - 'emotion' => [ - 'name' => [ - 'de' => 'Dankbarkeit', - 'en' => 'Gratitude', - ], - 'icon' => 'lucide-hands', - 'color' => '#22c55e', - 'sort_order' => 50, - ], - 'difficulty' => 'easy', - 'sort_order' => 20, - ], - ], - ], - ]; + $definitions = $this->definitions(); + $collectionMap = []; - DB::transaction(function () use ($collections) { - foreach ($collections as $definition) { + DB::transaction(function () use ($definitions, &$collectionMap) { + foreach ($definitions as $eventTypeSlug => $definition) { $eventType = $this->ensureEventType($definition['event_type']); $collection = TaskCollection::updateOrCreate( - ['slug' => $definition['slug']], + ['slug' => $definition['collection']['slug']], [ 'tenant_id' => null, 'event_type_id' => $eventType->id, - 'name_translations' => $definition['name'], - 'description_translations' => $definition['description'], - 'is_default' => $definition['is_default'] ?? false, - 'position' => $definition['position'] ?? 0, + 'name_translations' => $definition['collection']['name'], + 'description_translations' => $definition['collection']['description'], + 'is_default' => $definition['collection']['is_default'] ?? false, + 'position' => $definition['collection']['position'] ?? 0, + 'source_collection_id' => null, ] ); $syncPayload = []; - foreach ($definition['tasks'] as $taskDefinition) { - $emotion = $this->ensureEmotion($taskDefinition['emotion'] ?? [], $eventType->id); - - $task = Task::updateOrCreate( - ['slug' => $taskDefinition['slug']], - [ - 'tenant_id' => null, - 'event_type_id' => $eventType->id, - 'collection_id' => $collection->id, - 'emotion_id' => $emotion?->id, - 'title' => $taskDefinition['title'], - 'description' => $taskDefinition['description'] ?? null, - 'example_text' => $taskDefinition['example'] ?? null, - 'difficulty' => $taskDefinition['difficulty'] ?? 'easy', - 'priority' => 'medium', - 'sort_order' => $taskDefinition['sort_order'] ?? 0, - 'is_active' => true, - 'is_completed' => false, - ] - ); - - $syncPayload[$task->id] = ['sort_order' => $taskDefinition['sort_order'] ?? 0]; + foreach ($definition['base_tasks'] as $index => $taskDefinition) { + $sortOrder = $taskDefinition['sort_order'] ?? (($index + 1) * 10); + $task = $this->upsertTask($collection, $eventType, $taskDefinition, $sortOrder); + $syncPayload[$task->id] = ['sort_order' => $sortOrder]; } - if (! empty($syncPayload)) { - $collection->tasks()->sync($syncPayload); - } + $this->ensureMinimumTasks( + $collection, + $eventType, + $syncPayload, + count($syncPayload), + self::MIN_TASKS_PER_EVENT_TYPE, + $eventTypeSlug + ); + + $collection->tasks()->sync($syncPayload); + $collectionMap[$eventType->id] = $collection; } }); + + $this->assignOrphanTasks($collectionMap); + } + + private function definitions(): array + { + return [ + 'wedding' => [ + 'event_type' => [ + 'slug' => 'wedding', + 'name' => ['de' => 'Hochzeit', 'en' => 'Wedding'], + 'icon' => 'lucide-heart', + ], + 'collection' => [ + 'slug' => 'wedding-classics', + 'name' => [ + 'de' => 'Hochzeitsklassiker', + 'en' => 'Wedding Classics', + ], + 'description' => [ + 'de' => 'Kuratierte Aufgaben rund um Trauung, Emotionen und besondere Momente.', + 'en' => 'Curated prompts for vows, emotions, and memorable wedding highlights.', + ], + 'is_default' => true, + 'position' => 10, + ], + 'base_tasks' => [ + $this->taskDefinition( + 'wedding-first-look', + ['de' => 'Erster Blick des Brautpaares festhalten', 'en' => 'Capture the couple’s first look'], + ['de' => 'Halte den Moment fest, in dem sich Braut und Bräutigam zum ersten Mal sehen.', 'en' => 'Capture the instant when the couple sees each other for the first time.'], + ['de' => 'Fotografiere die Reaktionen aus verschiedenen Blickwinkeln.', 'en' => 'Shoot reactions from multiple angles.'], + ['name' => ['de' => 'Romantik', 'en' => 'Romance'], 'icon' => 'lucide-heart', 'color' => '#ec4899', 'sort_order' => 10], + 'easy', + 10 + ), + $this->taskDefinition( + 'wedding-family-hug', + ['de' => 'Familienumarmung organisieren', 'en' => 'Organise a family group hug'], + ['de' => 'Bitte die wichtigsten Menschen, das Paar gleichzeitig zu umarmen.', 'en' => 'Ask the closest friends and family to hug the couple all at once.'], + ['de' => 'Kombiniere die Umarmung mit einem Toast.', 'en' => 'Combine the hug with a heartfelt toast.'], + ['name' => ['de' => 'Freude', 'en' => 'Joy'], 'icon' => 'lucide-smile', 'color' => '#f59e0b', 'sort_order' => 20], + 'medium', + 20 + ), + $this->taskDefinition( + 'wedding-midnight-sparkler', + ['de' => 'Mitternachtsfunkeln mit Wunderkerzen', 'en' => 'Midnight sparkler moment'], + ['de' => 'Verteile Wunderkerzen und schafft ein leuchtendes Spalier für das Paar.', 'en' => 'Hand out sparklers and form a glowing aisle for the couple.'], + ['de' => 'Koordiniere die Musik und kündige den Countdown an.', 'en' => 'Coordinate music and announce a countdown.'], + ['name' => ['de' => 'Ekstase', 'en' => 'Euphoria'], 'icon' => 'lucide-stars', 'color' => '#6366f1', 'sort_order' => 30], + 'medium', + 30 + ), + $this->taskDefinition( + 'wedding-vow-whisper', + ['de' => 'Flüstert die liebsten Gelübde erneut', 'en' => 'Whisper your vows again'], + ['de' => 'Lasst das Paar sich gegenseitig einen Satz aus den Gelübden zuflüstern.', 'en' => 'Let the couple whisper a favourite line from their vows to each other.'], + ['de' => 'Nahaufnahme der Gesichter, während sie lächeln.', 'en' => 'Capture a close-up of the smiles while they whisper.'], + ['name' => ['de' => 'Verbundenheit', 'en' => 'Connection'], 'icon' => 'lucide-infinity', 'color' => '#a855f7', 'sort_order' => 35], + 'easy', + 40 + ), + $this->taskDefinition( + 'wedding-generations-portrait', + ['de' => 'Generationenportrait', 'en' => 'Generations portrait'], + ['de' => 'Bringe drei Generationen gleichzeitig aufs Foto – vom jüngsten zum ältesten Familienmitglied.', 'en' => 'Bring three generations into a single photograph – youngest to oldest.'], + ['de' => 'Stellt sie gestaffelt auf einer Treppe auf.', 'en' => 'Arrange them on steps for a layered look.'], + ['name' => ['de' => 'Familie', 'en' => 'Family'], 'icon' => 'lucide-users', 'color' => '#0ea5e9', 'sort_order' => 45], + 'medium', + 50 + ), + ], + ], + 'birthday' => [ + 'event_type' => [ + 'slug' => 'birthday', + 'name' => ['de' => 'Geburtstag', 'en' => 'Birthday'], + 'icon' => 'lucide-cake', + ], + 'collection' => [ + 'slug' => 'birthday-celebration', + 'name' => [ + 'de' => 'Geburtstags-Highlights', + 'en' => 'Birthday Highlights', + ], + 'description' => [ + 'de' => 'Aufgaben für Überraschungen, Gratulationen und gemeinsames Feiern.', + 'en' => 'Prompts covering surprises, wishes, and shared celebrations.', + ], + 'is_default' => false, + 'position' => 20, + ], + 'base_tasks' => [ + $this->taskDefinition( + 'birthday-surprise-wall', + ['de' => 'Überraschungswand mit Polaroids gestalten', 'en' => 'Create a surprise wall filled with instant photos'], + ['de' => 'Sammle Schnappschüsse der Gäste und befestige sie als Fotowand.', 'en' => 'Collect snapshots from guests and display them as a photo wall.'], + ['de' => 'Bitte jeden Gast um einen kurzen Gruß zum Bild.', 'en' => 'Invite each guest to add a short note to the photo.'], + ['name' => ['de' => 'Nostalgie', 'en' => 'Nostalgia'], 'icon' => 'lucide-images', 'color' => '#f97316', 'sort_order' => 40], + 'easy', + 10 + ), + $this->taskDefinition( + 'birthday-toast-circle', + ['de' => 'Gratulationskreis mit kurzen Toasts', 'en' => 'Circle of toasts'], + ['de' => 'Bildet einen Kreis und bittet jede Person um einen 10-Sekunden-Toast.', 'en' => 'Gather in a circle and ask each guest for a 10-second toast.'], + ['de' => 'Nimm die Lautstärke nach jedem Toast auf – wer jubelt am lautesten?', 'en' => 'Capture the volume of each cheer—who is the loudest?'], + ['name' => ['de' => 'Dankbarkeit', 'en' => 'Gratitude'], 'icon' => 'lucide-hands', 'color' => '#22c55e', 'sort_order' => 50], + 'easy', + 20 + ), + $this->taskDefinition( + 'birthday-memory-lane', + ['de' => 'Memory-Lane Foto-Story', 'en' => 'Memory lane photo story'], + ['de' => 'Suche drei Gegenstände, die zum Geburtstagsgast passen, und erzähle eine Mini-Story mit Fotos.', 'en' => 'Find three objects that represent the guest of honour and photograph them in a mini story.'], + ['de' => 'Nutze unterschiedliche Bildausschnitte für die Story.', 'en' => 'Use different framings to tell the story.'], + ['name' => ['de' => 'Erinnerung', 'en' => 'Reminiscence'], 'icon' => 'lucide-book-open', 'color' => '#6366f1', 'sort_order' => 55], + 'medium', + 30 + ), + ], + ], + 'christmas' => [ + 'event_type' => [ + 'slug' => 'christmas', + 'name' => ['de' => 'Weihnachten', 'en' => 'Christmas'], + 'icon' => 'lucide-sparkles', + ], + 'collection' => [ + 'slug' => 'christmas-magic', + 'name' => [ + 'de' => 'Festliche Highlights', + 'en' => 'Festive Highlights', + ], + 'description' => [ + 'de' => 'Wärmende Aufgaben für Glühwein-Momente und gemeinsames Staunen.', + 'en' => 'Cozy prompts for mulled wine moments and shared wonder.', + ], + 'is_default' => false, + 'position' => 30, + ], + 'base_tasks' => [ + $this->taskDefinition( + 'christmas-light-hunt', + ['de' => 'Finde das schönste Lichterdekor', 'en' => 'Spot the brightest decoration'], + ['de' => 'Halte das stimmungsvollste Licht im Raum fest.', 'en' => 'Capture the coziest lights around you.'], + ['de' => 'Zeige die Lichtquelle plus Reaktion eines Gastes.', 'en' => 'Show the light source together with a guest reacting to it.'], + ['name' => ['de' => 'Besinnlichkeit', 'en' => 'Serenity'], 'icon' => 'lucide-candle', 'color' => '#f97316', 'sort_order' => 20], + 'easy', + 10 + ), + $this->taskDefinition( + 'christmas-cookie-cheers', + ['de' => 'Plätzchen-Toast', 'en' => 'Cookie cheers'], + ['de' => 'Stoßt mit Lieblingsplätzchen an und haltet das gemeinsame Lachen fest.', 'en' => 'Clink your favourite cookies together and capture the laughter.'], + ['de' => 'Fotografiere in einer Reihe – Hände vorne, Gesichter dahinter.', 'en' => 'Frame hands with cookies in front, smiling faces behind.'], + ['name' => ['de' => 'Wärme', 'en' => 'Warmth'], 'icon' => 'lucide-mug-hot', 'color' => '#ef4444', 'sort_order' => 30], + 'easy', + 20 + ), + $this->taskDefinition( + 'christmas-snow-story', + ['de' => 'Schneegestöber im Innenraum', 'en' => 'Indoor snow story'], + ['de' => 'Nutze Deko oder Papier, um Schneeflocken zu simulieren, und halte den Zauber fest.', 'en' => 'Use props or paper to fake snow and capture the magic.'], + ['de' => 'Lass die Schneeflocken im Vordergrund unscharf erscheinen.', 'en' => 'Keep the fake snow in the foreground out of focus for a dreamy look.'], + ['name' => ['de' => 'Staunen', 'en' => 'Wonder'], 'icon' => 'lucide-sparkles', 'color' => '#22d3ee', 'sort_order' => 35], + 'medium', + 30 + ), + ], + ], + 'corporate' => [ + 'event_type' => [ + 'slug' => 'corporate', + 'name' => ['de' => 'Firma', 'en' => 'Corporate'], + 'icon' => 'lucide-briefcase', + ], + 'collection' => [ + 'slug' => 'corporate-connect', + 'name' => [ + 'de' => 'Team-Verbindungen', + 'en' => 'Team Connections', + ], + 'description' => [ + 'de' => 'Interaktive Aufgaben für produktive Offsites und Firmenfeiern.', + 'en' => 'Interactive prompts for productive offsites and company celebrations.', + ], + 'is_default' => false, + 'position' => 40, + ], + 'base_tasks' => [ + $this->taskDefinition( + 'corporate-high-five-chain', + ['de' => 'High-Five-Kette starten', 'en' => 'Start a high-five chain'], + ['de' => 'Platziert euch im Kreis und gebt reihum High-Fives – haltet den Moment fest.', 'en' => 'Form a circle and pass on high-fives – capture the energy.'], + ['de' => 'Nutze Serienaufnahmen, um alle Bewegungen einzufangen.', 'en' => 'Use burst mode to catch every high-five.'], + ['name' => ['de' => 'Teamgeist', 'en' => 'Team Spirit'], 'icon' => 'lucide-users', 'color' => '#0ea5e9', 'sort_order' => 10], + 'easy', + 10 + ), + $this->taskDefinition( + 'corporate-goal-toast', + ['de' => 'Ziele feiern', 'en' => 'Celebrate milestones'], + ['de' => 'Ruft gemeinsam ein erreichte Ziel in die Kamera und haltet die Euphorie fest.', 'en' => 'Shout a recent team achievement into the camera together.'], + ['de' => 'Lasst Konfetti oder Luftballons in den Rahmen fallen.', 'en' => 'Drop confetti or balloons into the frame.'], + ['name' => ['de' => 'Stolz', 'en' => 'Pride'], 'icon' => 'lucide-trophy', 'color' => '#22c55e', 'sort_order' => 20], + 'medium', + 20 + ), + $this->taskDefinition( + 'corporate-coffee-pose', + ['de' => 'Kaffee-Powerpose', 'en' => 'Coffee power pose'], + ['de' => 'Lass das Team mit Kaffeebechern posieren – jede Person zeigt eine andere Emotion.', 'en' => 'Have the team pose with coffee mugs, each showing a different emotion.'], + ['de' => 'Nutze eine niedrige Perspektive, um Power auszudrücken.', 'en' => 'Shoot from a low angle to emphasise power.'], + ['name' => ['de' => 'Motivation', 'en' => 'Motivation'], 'icon' => 'lucide-rocket', 'color' => '#8b5cf6', 'sort_order' => 30], + 'easy', + 30 + ), + ], + ], + ]; + } + + private function autoTaskSeeds(): array + { + return [ + 'wedding' => [ + $this->autoSeed('dancefloor', + ['de' => 'Dancefloor-Glück #{n}', 'en' => 'Dancefloor joy #{n}'], + ['de' => 'Fange eine wilde Tanzszene ein, die die Energie der Gäste zeigt.', 'en' => 'Capture a wild dance scene packed with guest energy.'], + ['de' => 'Nutze eine lange Belichtungszeit für Lichtstreifen.', 'en' => 'Use a longer exposure to create light trails.'], + ['name' => ['de' => 'Ekstase', 'en' => 'Euphoria'], 'icon' => 'lucide-stars', 'color' => '#a855f7'], + 'medium' + ), + $this->autoSeed('sparkler', + ['de' => 'Wunderkerzen-Story #{n}', 'en' => 'Sparkler story #{n}'], + ['de' => 'Erzähle die Geschichte eines funkelnden Moments mit mindestens drei Bildern.', 'en' => 'Tell the story of a sparkling moment in three frames.'], + ['de' => 'Variiere zwischen Detailaufnahme, Halbtotaler und Reaktion.', 'en' => 'Mix a detail shot, a medium frame, and a reaction close-up.'], + ['name' => ['de' => 'Staunen', 'en' => 'Awe'], 'icon' => 'lucide-wand', 'color' => '#f97316'], + 'medium' + ), + $this->autoSeed('first-dance-detail', + ['de' => 'Detail beim Hochzeitstanz #{n}', 'en' => 'First-dance detail #{n}'], + ['de' => 'Suche ein kleines Detail während des Hochzeitstanzes – Hände, Schuhe, Accessoires.', 'en' => 'Spot a tiny detail during the first dance – hands, shoes or accessories.'], + ['de' => 'Nutze starkes Bokeh für den Hintergrund.', 'en' => 'Use heavy bokeh to blur the background.'], + ['name' => ['de' => 'Zärtlichkeit', 'en' => 'Tenderness'], 'icon' => 'lucide-feather', 'color' => '#fb7185'], + 'easy' + ), + $this->autoSeed('guest-story', + ['de' => 'Gästegeschichte #{n}', 'en' => 'Guest story #{n}'], + ['de' => 'Dokumentiere eine Gruppe von Gästen, wie sie miteinander interagiert.', 'en' => 'Document a group of guests interacting naturally.'], + ['de' => 'Fotografiere die Gruppe aus zwei Perspektiven.', 'en' => 'Capture the group from two perspectives.'], + ['name' => ['de' => 'Freundschaft', 'en' => 'Friendship'], 'icon' => 'lucide-users', 'color' => '#38bdf8'], + 'easy' + ), + ], + 'birthday' => [ + $this->autoSeed('candle-wish', + ['de' => 'Kerzenwunsch #{n}', 'en' => 'Candle wish #{n}'], + ['de' => 'Halte den Moment fest, in dem der Wunsch ausgesprochen wird.', 'en' => 'Capture the second the wish is made.'], + ['de' => 'Lass das Motiv in den Kerzenlichtschein eintauchen.', 'en' => 'Bathe the scene in candlelight glow.'], + ['name' => ['de' => 'Freude', 'en' => 'Joy'], 'icon' => 'lucide-sparkles', 'color' => '#facc15'], + 'easy' + ), + $this->autoSeed('gift-reaction', + ['de' => 'Geschenk-Reaktion #{n}', 'en' => 'Gift reaction #{n}'], + ['de' => 'Fange die spontanste Reaktion auf ein Geschenk ein.', 'en' => 'Capture the most spontaneous gift reaction.'], + ['de' => 'Versuche eine Serienaufnahme für echte Emotionen.', 'en' => 'Use burst mode for authentic emotion.'], + ['name' => ['de' => 'Überraschung', 'en' => 'Surprise'], 'icon' => 'lucide-gift', 'color' => '#f472b6'], + 'easy' + ), + $this->autoSeed('birthday-anthem', + ['de' => 'Geburtstagshymne #{n}', 'en' => 'Birthday anthem #{n}'], + ['de' => 'Dokumentiere das Mitsingen eines Liedes inklusive Stimmung im Raum.', 'en' => 'Document everyone singing a song and the overall mood.'], + ['de' => 'Fokussiere auf die Person, die am lautesten singt.', 'en' => 'Focus on whoever sings the loudest.'], + ['name' => ['de' => 'Lebensfreude', 'en' => 'Delight'], 'icon' => 'lucide-music', 'color' => '#60a5fa'], + 'medium' + ), + ], + 'christmas' => [ + $this->autoSeed('ornament-closeup', + ['de' => 'Ornament-Kunstwerk #{n}', 'en' => 'Ornament art #{n}'], + ['de' => 'Inszeniere ein besonderes Ornament in Nahaufnahme.', 'en' => 'Stage a special ornament in a moody close-up.'], + ['de' => 'Lass warme Lichter im Hintergrund bokeh-artig wirken.', 'en' => 'Let warm lights blur softly in the background.'], + ['name' => ['de' => 'Staunen', 'en' => 'Wonder'], 'icon' => 'lucide-sparkles', 'color' => '#38bdf8'], + 'easy' + ), + $this->autoSeed('carol-chorus', + ['de' => 'Singender Chor #{n}', 'en' => 'Carolling chorus #{n}'], + ['de' => 'Zeige eine Gruppe beim Singen eines Weihnachtsliedes.', 'en' => 'Highlight a group singing a seasonal song.'], + ['de' => 'Nutze eine Panorama-Aufnahme für alle Beteiligten.', 'en' => 'Use a panorama framing to include everyone.'], + ['name' => ['de' => 'Gemeinschaft', 'en' => 'Togetherness'], 'icon' => 'lucide-users', 'color' => '#22c55e'], + 'medium' + ), + $this->autoSeed('wishlist-doodle', + ['de' => 'Wunschlisten-Doodle #{n}', 'en' => 'Wishlist doodle #{n}'], + ['de' => 'Fotografiere eine improvisierte Wunschliste inklusive Zeichnungen.', 'en' => 'Photograph an improvised wishlist complete with doodles.'], + ['de' => 'Lege die Liste auf Holz- oder Stoffoberflächen für Wärme.', 'en' => 'Place the list on wood or fabric for extra warmth.'], + ['name' => ['de' => 'Hoffnung', 'en' => 'Hope'], 'icon' => 'lucide-heart', 'color' => '#f97316'], + 'easy' + ), + ], + 'corporate' => [ + $this->autoSeed('team-brainstorm', + ['de' => 'Brainstorm-Blitzlicht #{n}', 'en' => 'Brainstorm spotlight #{n}'], + ['de' => 'Halte eine kreative Idee auf einem Whiteboard plus das Team dahinter fest.', 'en' => 'Capture a clever whiteboard idea with the team behind it.'], + ['de' => 'Arbeite mit Spiegelungen im Glas oder Bildschirm.', 'en' => 'Play with reflections on glass or screens.'], + ['name' => ['de' => 'Innovation', 'en' => 'Innovation'], 'icon' => 'lucide-lightbulb', 'color' => '#facc15'], + 'medium' + ), + $this->autoSeed('watercooler-moment', + ['de' => 'Watercooler-Moment #{n}', 'en' => 'Watercooler moment #{n}'], + ['de' => 'Zeige Kollegen beim lockeren Austausch abseits des Meetings.', 'en' => 'Show teammates chatting casually away from the meeting table.'], + ['de' => 'Fokussiere auf Gestik und Körpersprache.', 'en' => 'Focus on gestures and body language.'], + ['name' => ['de' => 'Leichtigkeit', 'en' => 'Ease'], 'icon' => 'lucide-coffee', 'color' => '#60a5fa'], + 'easy' + ), + $this->autoSeed('celebration-confetti', + ['de' => 'Konfetti-Erfolg #{n}', 'en' => 'Confetti success #{n}'], + ['de' => 'Inszeniere einen kleinen Konfettiwurf als Symbol für Team-Erfolg.', 'en' => 'Stage a mini confetti throw to celebrate team success.'], + ['de' => 'Arbeite mit schneller Verschlusszeit für eingefrorene Partikel.', 'en' => 'Use a fast shutter to freeze the confetti.'], + ['name' => ['de' => 'Erfolg', 'en' => 'Success'], 'icon' => 'lucide-flag', 'color' => '#a855f7'], + 'medium' + ), + ], + 'default' => [ + $this->autoSeed('story-fragments', + ['de' => 'Story-Fragmente #{n}', 'en' => 'Story fragments #{n}'], + ['de' => 'Finde drei kleine Details, die zusammen eine Geschichte ergeben.', 'en' => 'Find three small details that build one story.'], + ['de' => 'Kombiniere Nahaufnahmen mit einem Übersichtsfoto.', 'en' => 'Combine close-ups with one establishing shot.'], + ['name' => ['de' => 'Neugier', 'en' => 'Curiosity'], 'icon' => 'lucide-eye', 'color' => '#38bdf8'], + 'easy' + ), + ], + ]; + } + + private function taskDefinition( + string $slug, + array $title, + array $description, + array $example, + ?array $emotion, + string $difficulty, + int $sortOrder + ): array { + return [ + 'slug' => $slug, + 'title' => $title, + 'description' => $description, + 'example' => $example, + 'emotion' => $emotion, + 'difficulty' => $difficulty, + 'sort_order' => $sortOrder, + ]; + } + + private function autoSeed( + string $slug, + array $title, + array $description, + array $example, + ?array $emotion, + string $difficulty + ): array { + return [ + 'slug' => $slug, + 'title' => $title, + 'description' => $description, + 'example' => $example, + 'emotion' => $emotion, + 'difficulty' => $difficulty, + ]; + } + + private function upsertTask(TaskCollection $collection, EventType $eventType, array $definition, int $sortOrder): Task + { + $emotion = $this->ensureEmotion($definition['emotion'] ?? [], $eventType->id); + + $task = Task::updateOrCreate( + ['slug' => $definition['slug']], + [ + 'tenant_id' => null, + 'event_type_id' => $eventType->id, + 'collection_id' => $collection->id, + 'emotion_id' => $emotion?->id, + 'title' => $definition['title'], + 'description' => $definition['description'] ?? null, + 'example_text' => $definition['example'] ?? null, + 'difficulty' => $definition['difficulty'] ?? 'easy', + 'priority' => $definition['priority'] ?? 'medium', + 'sort_order' => $definition['sort_order'] ?? $sortOrder, + 'is_active' => true, + 'is_completed' => false, + ] + ); + + if ($task->collection_id !== $collection->id) { + $task->collection_id = $collection->id; + $task->save(); + } + + return $task; + } + + private function ensureMinimumTasks( + TaskCollection $collection, + EventType $eventType, + array &$syncPayload, + int $existingCount, + int $minimum, + string $eventTypeSlug + ): void { + $needed = $minimum - $existingCount; + if ($needed <= 0) { + return; + } + + $seeds = $this->autoTaskSeeds()[$eventTypeSlug] ?? $this->autoTaskSeeds()['default']; + if (empty($seeds)) { + $seeds = $this->autoTaskSeeds()['default']; + } + + $baseCount = $existingCount; + + for ($i = 0; $i < $needed; $i++) { + $seed = $seeds[$i % count($seeds)]; + $sequence = $baseCount + $i + 1; + + $slug = Str::slug(sprintf('%s-%s-%s', $eventType->slug ?? $eventTypeSlug, $seed['slug'], $sequence)); + $taskDefinition = [ + 'slug' => $slug, + 'title' => [ + 'de' => str_replace('{n}', (string) $sequence, $seed['title']['de']), + 'en' => str_replace('{n}', (string) $sequence, $seed['title']['en']), + ], + 'description' => [ + 'de' => str_replace('{n}', (string) $sequence, $seed['description']['de']), + 'en' => str_replace('{n}', (string) $sequence, $seed['description']['en']), + ], + 'example' => [ + 'de' => str_replace('{n}', (string) $sequence, $seed['example']['de']), + 'en' => str_replace('{n}', (string) $sequence, $seed['example']['en']), + ], + 'emotion' => $seed['emotion'] ?? null, + 'difficulty' => $seed['difficulty'] ?? 'easy', + 'priority' => $seed['priority'] ?? 'medium', + 'sort_order' => 500 + ($sequence * 5), + ]; + + $task = $this->upsertTask($collection, $eventType, $taskDefinition, $taskDefinition['sort_order']); + $syncPayload[$task->id] = ['sort_order' => $taskDefinition['sort_order']]; + } + } + + private function assignOrphanTasks(array $collectionMap): void + { + if (empty($collectionMap)) { + return; + } + + Task::whereNull('collection_id') + ->orderBy('id') + ->chunkById(100, function ($tasks) use ($collectionMap) { + foreach ($tasks as $task) { + $collection = $collectionMap[$task->event_type_id] ?? Arr::first($collectionMap); + if (! $collection) { + continue; + } + + $task->collection_id = $collection->id; + $task->save(); + + $collection->tasks()->syncWithoutDetaching([ + $task->id => ['sort_order' => $task->sort_order ?? 0], + ]); + } + }); } protected function ensureEventType(array $definition): EventType { - $payload = [ - 'name' => $definition['name'], - 'icon' => $definition['icon'] ?? null, - ]; - return EventType::updateOrCreate( ['slug' => $definition['slug']], - $payload + [ + 'name' => $definition['name'], + 'icon' => $definition['icon'] ?? null, + ] ); } @@ -261,7 +557,6 @@ class TaskCollectionsSeeder extends Seeder } $query = Emotion::query(); - $name = $definition['name'] ?? []; if (isset($name['en'])) { diff --git a/package-lock.json b/package-lock.json index 6903a40..20ed197 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,8 @@ "embla-carousel": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", + "fabric": "^6.0.1", + "fabricjs-design-tool": "github:rifrocket/fabricjs-design-tool#main", "globals": "^15.14.0", "html5-qrcode": "^2.3.8", "i18next": "^25.5.3", @@ -51,10 +53,12 @@ "i18next-http-backend": "^3.0.2", "laravel-vite-plugin": "^2.0", "lucide-react": "^0.475.0", + "pdf-lib": "^1.17.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.6.0", "react-i18next": "^16.0.0", + "react-rnd": "^10.4.12", "react-router-dom": "^7.8.2", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", @@ -69,6 +73,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.5.2", + "@types/fabric": "^5.3.9", "@types/node": "^22.13.5", "eslint": "^9.17.0", "eslint-config-prettier": "^10.0.1", @@ -97,6 +102,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@antfu/ni": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-25.0.0.tgz", @@ -1769,6 +1786,75 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1833,6 +1919,139 @@ "dev": true, "license": "MIT" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.19.1.tgz", @@ -1982,7 +2201,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1996,7 +2214,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2006,7 +2223,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2073,6 +2289,46 @@ "hi-base32": "^0.5.0" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/standard-fonts/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@pdf-lib/upng/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@playwright/mcp": { "version": "0.0.37", "resolved": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.37.tgz", @@ -4048,6 +4304,16 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -4144,6 +4410,12 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/fabric": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/@types/fabric/-/fabric-5.3.10.tgz", + "integrity": "sha512-fsJIuVkU+B2AnmQh+Ml2X0ax3NmRIqLvEXmZ+squX60HaF89TvdIP6tI6Uk5srXaauswTwPOOfWE7k2QboUZCg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4176,6 +4448,19 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", @@ -4201,6 +4486,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -4612,6 +4904,21 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -4650,7 +4957,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4675,6 +4982,17 @@ "acorn": "^6 || ^7 || ^8" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4752,7 +5070,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -4790,7 +5108,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4824,11 +5141,16 @@ "node": ">=14" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4842,7 +5164,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -4851,6 +5172,34 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5101,7 +5450,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -5187,6 +5535,16 @@ "bare-path": "^3.0.0" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5225,6 +5583,18 @@ "node": ">=10.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -5261,7 +5631,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5272,7 +5642,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -5424,6 +5793,15 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001748", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", @@ -5444,6 +5822,42 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -5499,6 +5913,42 @@ "node": ">= 16" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -5724,6 +6174,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5750,7 +6210,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/concurrently": { @@ -5777,6 +6237,13 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -5822,6 +6289,18 @@ "node": ">=6.6.0" } }, + "node_modules/core-js": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -5891,6 +6370,16 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -5898,6 +6387,25 @@ "dev": true, "license": "MIT" }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "license": "MIT", + "optional": true + }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -6071,9 +6579,22 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dedent": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", @@ -6187,6 +6708,13 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6227,6 +6755,12 @@ "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", "license": "BSD-3-Clause" }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, "node_modules/diff": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", @@ -6237,6 +6771,12 @@ "node": ">=0.3.1" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -6258,6 +6798,40 @@ "license": "MIT", "peer": true }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "license": "MIT", + "optional": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -6285,6 +6859,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/eciesjs": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.15.tgz", @@ -6401,7 +6981,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -7155,6 +7735,368 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fabric": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.7.1.tgz", + "integrity": "sha512-dLxSmIvN4InJf4xOjbl1LFWh8WGOUIYtcuDIGs2IN0Z9lI0zGobfesDauyEhI1+owMLTPCCiEv01rpYXm7g2EQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "jsdom": "^20.0.1" + } + }, + "node_modules/fabric/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/fabric/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "license": "MIT", + "optional": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fabric/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "license": "MIT", + "optional": true + }, + "node_modules/fabric/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabric/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabric/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fabric/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fabric/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/fabric/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/fabric/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fabric/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabric/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fabric/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "license": "MIT", + "optional": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/fabric/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabric/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabric/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabric/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabric/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/fabricjs-design-tool": { + "name": "@rifrocket/fabricjs-design-tool", + "version": "1.0.3", + "resolved": "git+ssh://git@github.com/rifrocket/fabricjs-design-tool.git#61d6c65d527f1a4ae5031d4bda7b2e797d82ff65", + "license": "MIT", + "dependencies": { + "@types/fabric": "^5.3.10", + "fabric": "^6.6.7", + "jspdf": "^3.0.1", + "lucide-react": "^0.513.0", + "qr-code-styling": "^1.9.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^3.4.17" + } + }, + "node_modules/fabricjs-design-tool/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/fabricjs-design-tool/node_modules/lucide-react": { + "version": "0.513.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.513.0.tgz", + "integrity": "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/fabricjs-design-tool/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fabricjs-design-tool/node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7171,7 +8113,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7188,7 +8129,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7210,11 +8150,21 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -7270,6 +8220,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -7303,7 +8259,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -7403,6 +8358,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -7465,6 +8436,39 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/fs-mkdirp-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", @@ -7479,6 +8483,13 @@ "node": ">=10.13.0" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -7547,6 +8558,80 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -7705,11 +8790,32 @@ "node": ">= 14" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7950,6 +9056,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7997,6 +9110,20 @@ "void-elements": "3.1.0" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/html5-qrcode": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", @@ -8225,6 +9352,18 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -8246,6 +9385,12 @@ "node": ">= 0.4" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/ip-address": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", @@ -8324,6 +9469,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -8358,7 +9515,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -8409,7 +9565,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8464,7 +9619,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -8533,7 +9687,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -8599,7 +9752,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-promise": { @@ -8843,6 +9996,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -9057,6 +10225,23 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jspdf": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", + "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9394,6 +10579,18 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9536,6 +10733,22 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9577,7 +10790,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -9587,7 +10799,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -9601,7 +10812,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9654,6 +10864,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9668,7 +10891,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9714,6 +10937,19 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9776,6 +11012,24 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9866,11 +11120,26 @@ "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9919,11 +11188,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nwsapi": { "version": "2.2.22", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -9935,6 +11218,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10223,6 +11515,12 @@ "node": ">= 14" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/package-manager-detector": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.0.tgz", @@ -10230,6 +11528,12 @@ "dev": true, "license": "MIT" }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10306,6 +11610,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10319,9 +11633,30 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -10346,12 +11681,43 @@ "node": ">= 14.16" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10370,6 +11736,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -10461,6 +11845,154 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10760,6 +12292,19 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -10818,6 +12363,24 @@ "node": ">=18" } }, + "node_modules/qr-code-styling": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", + "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", + "license": "MIT", + "dependencies": { + "qrcode-generator": "^1.4.4" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/qrcode-generator": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz", + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -10833,11 +12396,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT", + "optional": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -10854,6 +12423,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10894,6 +12473,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/re-resizable": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", + "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -10915,6 +12504,29 @@ "react": "^19.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react-hot-toast": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", @@ -11020,6 +12632,27 @@ } } }, + "node_modules/react-rnd": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", + "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", + "license": "MIT", + "dependencies": { + "re-resizable": "6.11.2", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/react-router": { "version": "7.9.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz", @@ -11080,11 +12713,20 @@ } } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -11095,6 +12737,30 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -11149,6 +12815,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -11196,6 +12869,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT", + "optional": true + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -11264,13 +12944,39 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", @@ -11362,7 +13068,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11476,7 +13181,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -11558,6 +13263,13 @@ "node": ">= 18" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/set-cookie-parser": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", @@ -11819,7 +13531,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -11828,6 +13539,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -11912,6 +13656,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -11987,7 +13741,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -12011,6 +13765,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -12131,7 +13927,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -12143,6 +13938,28 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12192,6 +14009,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -12211,7 +14103,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12220,11 +14111,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tabbable": { @@ -12340,6 +14241,37 @@ "b4a": "^1.6.4" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -12447,7 +14379,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -12519,6 +14450,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, "node_modules/ts-morph": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", @@ -12883,6 +14820,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -12939,9 +14887,18 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/value-or-function": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", @@ -14470,6 +16427,61 @@ "node": ">=8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14495,6 +16507,65 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -14581,7 +16652,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/xtend": { diff --git a/package.json b/package.json index 0f14e71..ae17232 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.5.2", + "@types/fabric": "^5.3.9", "@types/node": "^22.13.5", "eslint": "^9.17.0", "eslint-config-prettier": "^10.0.1", @@ -82,9 +83,12 @@ "i18next-http-backend": "^3.0.2", "laravel-vite-plugin": "^2.0", "lucide-react": "^0.475.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-rnd": "^10.4.12", + "fabric": "^6.0.1", + "fabricjs-design-tool": "github:rifrocket/fabricjs-design-tool#main", + "pdf-lib": "^1.17.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-rnd": "^10.4.12", "react-hot-toast": "^2.6.0", "react-i18next": "^16.0.0", "react-router-dom": "^7.8.2", diff --git a/public/hot.bak b/public/hot.bak new file mode 100644 index 0000000..34adf08 --- /dev/null +++ b/public/hot.bak @@ -0,0 +1 @@ +http://localhost:5173 \ No newline at end of file diff --git a/resources/js/admin/api.ts b/resources/js/admin/api.ts index b6962ab..cf3594f 100644 --- a/resources/js/admin/api.ts +++ b/resources/js/admin/api.ts @@ -8,6 +8,12 @@ export type EventQrInviteLayout = { name: string; description: string; subtitle: string; + badge_label?: string | null; + instructions_heading?: string | null; + link_heading?: string | null; + cta_label?: string | null; + cta_caption?: string | null; + instructions?: string[]; preview: { background: string | null; background_gradient: { angle: number; stops: string[] } | null; @@ -16,7 +22,7 @@ export type EventQrInviteLayout = { qr_size_px?: number | null; }; formats: string[]; - download_urls: Record; + download_urls?: Record; }; export type TenantEventType = { @@ -675,6 +681,12 @@ function normalizeQrInvite(raw: JsonValue): EventQrInvite { name: String(layout.name ?? ''), description: String(layout.description ?? ''), subtitle: String(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: Array.isArray(layout.instructions) ? (layout.instructions as string[]) : [], preview: { background: layout.preview?.background ?? null, background_gradient: layout.preview?.background_gradient ?? null, diff --git a/resources/js/admin/components/DevTenantSwitcher.tsx b/resources/js/admin/components/DevTenantSwitcher.tsx index 3947d35..bd8d02e 100644 --- a/resources/js/admin/components/DevTenantSwitcher.tsx +++ b/resources/js/admin/components/DevTenantSwitcher.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Loader2 } from 'lucide-react'; +import { Loader2, PanelLeftClose, PanelRightOpen } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -22,11 +22,48 @@ declare global { export function DevTenantSwitcher() { const helper = window.fotospielDemoAuth; const [loggingIn, setLoggingIn] = React.useState(null); + const [collapsed, setCollapsed] = React.useState(() => { + if (typeof window === 'undefined') { + return false; + } + + try { + return window.localStorage.getItem('fotospiel-dev-switcher-collapsed') === '1'; + } catch (error) { + console.warn('[DevAuth] Failed to read collapse state', error); + return false; + } + }); + + React.useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + try { + window.localStorage.setItem('fotospiel-dev-switcher-collapsed', collapsed ? '1' : '0'); + } catch (error) { + console.warn('[DevAuth] Failed to persist collapse state', error); + } + }, [collapsed]); if (!helper) { return null; } + if (collapsed) { + return ( + + ); + } + async function handleLogin(key: string) { if (!helper) return; setLoggingIn(key); @@ -41,8 +78,18 @@ export function DevTenantSwitcher() { return (
- Demo tenants - Dev mode +
+ Demo tenants + Dev mode +
+

Select a seeded tenant to mint OAuth tokens and jump straight into their admin space. Available only in development builds. diff --git a/resources/js/admin/dev-tools.ts b/resources/js/admin/dev-tools.ts index 91ccf54..af74513 100644 --- a/resources/js/admin/dev-tools.ts +++ b/resources/js/admin/dev-tools.ts @@ -115,60 +115,72 @@ if (import.meta.env.DEV || import.meta.env.VITE_ENABLE_TENANT_SWITCHER === 'true globalThis.fotospielDemoAuth = api; } -function requestAuthorization(url: string, fallbackRedirect?: string): Promise { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.withCredentials = true; - xhr.setRequestHeader('Accept', 'application/json, text/plain, */*'); - xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - xhr.onreadystatechange = () => { - if (xhr.readyState !== XMLHttpRequest.DONE) { - return; +async function requestAuthorization(url: string, fallbackRedirect?: string): Promise { + const requestUrl = new URL(url, window.location.origin); + + let response: Response; + try { + response = await fetch(requestUrl.toString(), { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest', + }, + redirect: 'manual', + }); + } catch (error) { + throw new Error('Authorize request failed'); + } + + const status = response.status; + const isSuccess = (status >= 200 && status < 400) || status === 0; + if (!isSuccess) { + throw new Error(`Authorize failed with ${status}`); + } + + const contentType = response.headers.get('Content-Type') ?? ''; + if (contentType.includes('application/json')) { + try { + const payload = (await response.json()) as { + code?: string; + state?: string | null; + redirect_url?: string | null; + }; + + const target = payload.redirect_url ?? fallbackRedirect; + if (!target) { + throw new Error('Authorize response missing redirect target'); } - const contentType = xhr.getResponseHeader('Content-Type') ?? ''; - const responseUrl = xhr.responseURL || xhr.getResponseHeader('Location'); - if ((xhr.status >= 200 && xhr.status < 400) || xhr.status === 0) { - if (responseUrl) { - resolve(new URL(responseUrl, window.location.origin)); - return; - } - - if (contentType.includes('application/json')) { - try { - const payload = JSON.parse(xhr.responseText ?? '{}') as { - code?: string; - state?: string | null; - redirect_url?: string | null; - }; - const target = payload.redirect_url ?? fallbackRedirect; - if (!target) { - throw new Error('Authorize response missing redirect target'); - } - - const finalUrl = new URL(target, window.location.origin); - if (payload.code && !finalUrl.searchParams.has('code')) { - finalUrl.searchParams.set('code', payload.code); - } - if (payload.state && !finalUrl.searchParams.has('state')) { - finalUrl.searchParams.set('state', payload.state); - } - - resolve(finalUrl); - return; - } catch (error) { - reject(error instanceof Error ? error : new Error(String(error))); - return; - } - } + const finalUrl = new URL(target, window.location.origin); + if (payload.code && !finalUrl.searchParams.has('code')) { + finalUrl.searchParams.set('code', payload.code); + } + if (payload.state && !finalUrl.searchParams.has('state')) { + finalUrl.searchParams.set('state', payload.state); } - reject(new Error(`Authorize failed with ${xhr.status}`)); - }; - xhr.onerror = () => reject(new Error('Authorize request failed')); - xhr.send(); - }); + return finalUrl; + } catch (error) { + throw error instanceof Error ? error : new Error(String(error)); + } + } + + const locationHeader = response.headers.get('Location'); + if (locationHeader) { + return new URL(locationHeader, window.location.origin); + } + + if (response.url && response.url !== requestUrl.toString()) { + return new URL(response.url, window.location.origin); + } + + if (fallbackRedirect) { + return new URL(fallbackRedirect, window.location.origin); + } + + throw new Error('Authorize response missing redirect target'); } function verifyState(returnedState: string | null, expectedState: string): void { diff --git a/resources/js/admin/i18n/locales/de/auth.json b/resources/js/admin/i18n/locales/de/auth.json index cec8a23..1bfe804 100644 --- a/resources/js/admin/i18n/locales/de/auth.json +++ b/resources/js/admin/i18n/locales/de/auth.json @@ -1,10 +1,21 @@ { "login": { "title": "Tenant-Admin", - "lead": "Melde dich mit deinem Fotospiel-Account an. Du wirst zur sicheren OAuth-Anmeldung weitergeleitet und anschließend zur Admin-Oberfläche zurückgebracht.", - "cta": "Mit Tenant-Account anmelden", + "badge": "Fotospiel Tenant Admin", + "hero_title": "Event-Steuerung, die sich wie Zuhause anfühlt.", + "hero_subtitle": "Wechsle mühelos zwischen Mandanten, behalte Live-Uploads im Blick und teile elegante Einladungen – alles in einer ruhigen Oberfläche.", + "features": [ + "Gestalte QR-Einladungen und druckfertige Layouts in wenigen Klicks passend zu eurer Marke.", + "Organisiere Aufgaben, Emotionen und Sammlungen für jeden Eventtyp ohne Excel-Chaos.", + "Bleib am Eventtag souverän mit Dashboards, Live-Statistiken und sofortiger Moderation." + ], + "lead": "Die Anmeldung erfolgt über unseren sicheren OAuth-Login und bringt dich direkt wieder zurück.", + "panel_copy": "Melde dich mit deinen Fotospiel-Admin-Zugangsdaten an. Wir schützen dein Konto mit OAuth 2.1 und mandantenbewussten Berechtigungen.", + "cta": "Mit Fotospiel-Login fortfahren", "loading": "Bitte warten …", + "oauth_error_title": "Login aktuell nicht möglich", "oauth_error": "Anmeldung fehlgeschlagen: {{message}}", + "support": "Du brauchst Zugriff? Wende dich an den Tenant-Inhaber oder schreibe an support@fotospiel.de – wir helfen gern weiter.", "appearance_label": "Darstellung" } } diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index b876b04..fcea22f 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -283,12 +283,12 @@ }, "logo": { "label": "Logo", - "hint": "PNG oder SVG, max. 1 MB. Wird oben rechts platziert.", + "hint": "PNG, max. 1 MB. Wird oben rechts platziert.", "remove": "Logo entfernen" }, "preview": { "title": "Vorschau", - "hint": "Farben und Texte, wie sie im Layout erscheinen. Speichere, um neue PDFs/SVGs zu erhalten." + "hint": "Farben und Texte, wie sie im Layout erscheinen. Speichere, um neue PDFs/PNGs zu erhalten." }, "actions": { "save": "Speichern", @@ -299,7 +299,9 @@ "actionLabel": "Layout anpassen", "errors": { "logoTooLarge": "Das Logo darf maximal 1 MB groß sein.", - "noLayout": "Bitte wähle ein Layout aus." + "noLayout": "Bitte wähle ein Layout aus.", + "downloadFailed": "Download fehlgeschlagen. Bitte versuche es erneut.", + "printFailed": "Drucken konnte nicht gestartet werden." }, "defaults": { "badgeLabel": "Digitale Gästebox", @@ -310,7 +312,8 @@ "QR-Code scannen", "Profil anlegen", "Fotos teilen" - ] + ], + "textBlock": "Neuer Textblock – hier kannst du eigene Hinweise ergänzen." } } }, @@ -354,24 +357,35 @@ "export": { "title": "Drucken & Export", "description": "Lade druckfertige Dateien herunter oder starte direkt einen Testdruck.", + "mode": { + "standard": "Standardlayout", + "advanced": "Freier Editor" + }, + "previewHint": "Speichere deine Änderungen, um die Exportdateien neu zu erstellen.", + "noLayoutPreview": "Noch keine Vorschau verfügbar. Speichere das Layout zuerst.", "selectPlaceholder": "Einladung auswählen", "noInviteSelected": "Wähle zunächst eine Einladung aus, um Downloads zu starten.", "noLayouts": "Für diese Einladung sind aktuell keine Layouts verfügbar.", "actions": { - "print": "Direkt drucken" + "title": "Aktionen", + "description": "Starte deinen Testdruck oder lade die Layouts herunter.", + "printNow": "Direkt drucken", + "hint": "PDF enthält Beschnittmarken, PNG ist ideal für digitale Freigaben." }, "errorTitle": "Download fehlgeschlagen" }, "customizer": { "heading": "Layout anpassen", - "copy": "Verleihe der Einladung euren Ton: Texte, Farben und Logo lassen sich live bearbeiten.", + "copy": "Bearbeite Texte, Farben und Positionen direkt neben der Live-Vorschau. Änderungen werden sofort sichtbar.", "actions": { "save": "Layout speichern", "reset": "Zurücksetzen", "print": "Drucken", "removeLogo": "Logo entfernen", "uploadLogo": "Logo hochladen (max. 1 MB)", - "addInstruction": "Punkt hinzufügen" + "addInstruction": "Punkt hinzufügen", + "undo": "Rückgängig", + "redo": "Wiederholen" }, "sections": { "layouts": "Layouts", @@ -381,6 +395,37 @@ "instructionsHint": "Helft euren Gästen mit klaren Aufgaben. Maximal fünf Punkte.", "branding": "Farbgebung" }, + "elements": { + "title": "Elemente & Positionierung", + "hint": "Wähle ein Element aus, um es zu verschieben, anzupassen oder zu entfernen.", + "headline": "Überschrift", + "subtitle": "Untertitel", + "description": "Beschreibung", + "badge": "Badge", + "link": "Linkfeld", + "cta": "Call-to-Action", + "qr": "QR-Code", + "logo": "Logo", + "text": "Freier Textblock", + "remove": "Element entfernen", + "details": "Element-Details", + "detailsHint": "Passe Text, Ausrichtung und weitere Eigenschaften an. Änderungen werden sofort sichtbar.", + "customText": "Textinhalt", + "qrHint": "Der QR-Code lässt sich im Canvas in Größe und Position verändern.", + "logoHint": "Logos bearbeitest du im Bereich „Branding“. Hier kannst du das Element nur verschieben oder skalieren.", + "align": "Ausrichtung", + "alignLeft": "Links", + "alignCenter": "Zentriert", + "alignRight": "Rechts", + "fontSize": "Schriftgröße", + "selectHint": "Tippe ein Element im Canvas oder in der Liste an, um Details zu bearbeiten.", + "listHint": "Wähle ein Element aus, damit seine Einstellungen direkt darunter erscheinen.", + "addSubtitle": "Untertitel einblenden", + "addBadge": "Badge anzeigen", + "addLink": "Linkfeld hinzufügen", + "addCta": "Call-to-Action einfügen", + "addText": "Freien Textblock hinzufügen" + }, "fields": { "headline": "Überschrift", "subtitle": "Unterzeile", @@ -415,6 +460,113 @@ "layoutFallback": "Layout" } }, + "events": { + "errors": { + "missingSlug": "Kein Event ausgewählt.", + "loadFailed": "Event konnte nicht geladen werden.", + "notFoundTitle": "Event nicht gefunden", + "notFoundBody": "Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.", + "toggleFailed": "Status konnte nicht angepasst werden." + }, + "alerts": { + "failedTitle": "Aktion fehlgeschlagen" + }, + "placeholders": { + "untitled": "Unbenanntes Event" + }, + "actions": { + "backToList": "Zurück zur Liste", + "edit": "Bearbeiten", + "members": "Team & Rollen", + "tasks": "Aufgaben verwalten", + "invites": "Einladungen & Layouts", + "photos": "Fotos moderieren", + "refresh": "Aktualisieren" + }, + "workspace": { + "detailSubtitle": "Behalte Status, Aufgaben und Einladungen deines Events im Blick.", + "toolkitSubtitle": "Moderation, Aufgaben und Einladungen für deinen Eventtag bündeln.", + "sections": { + "statusTitle": "Eventstatus & Sichtbarkeit", + "statusSubtitle": "Aktiviere dein Event für Gäste oder verstecke es vorübergehend." + }, + "fields": { + "status": "Status", + "active": "Aktiv für Gäste", + "date": "Eventdatum", + "eventType": "Event-Typ", + "insights": "Letzte Aktivität", + "uploadsTotal": "{{count}} Uploads gesamt", + "uploadsToday": "{{count}} Uploads (24h)", + "likesTotal": "{{count}} Likes vergeben" + }, + "actions": { + "pause": "Event pausieren", + "activate": "Event aktivieren" + }, + "activeYes": "Ja", + "activeNo": "Nein" + }, + "status": { + "published": "Veröffentlicht", + "draft": "Entwurf", + "archived": "Archiviert" + }, + "quickActions": { + "title": "Schnellaktionen", + "subtitle": "Nutze die wichtigsten Schritte vor und während deines Events.", + "moderate": "Fotos moderieren", + "tasks": "Aufgaben bearbeiten", + "invites": "Layouts & QR verwalten", + "roles": "Team & Rollen anpassen", + "print": "Layouts als PDF drucken", + "toggle": "Status ändern" + }, + "metrics": { + "uploadsTotal": "Uploads gesamt", + "uploads24h": "Uploads (24h)", + "pending": "Fotos in Moderation", + "activeInvites": "Aktive Einladungen" + }, + "invites": { + "title": "QR-Einladungen", + "subtitle": "Behält aktive Einladungen und Layouts im Blick.", + "activeCount": "{{count}} aktiv", + "totalCount": "{{count}} gesamt", + "empty": "Noch keine Einladungen erstellt.", + "manage": "Layouts & Einladungen verwalten" + }, + "tasks": { + "title": "Aktive Aufgaben", + "subtitle": "Motiviere Gäste mit klaren Aufgaben & Highlights.", + "summary": "{{completed}} von {{total}} erledigt", + "empty": "Noch keine Aufgaben zugewiesen.", + "manage": "Aufgabenbereich öffnen" + }, + "photos": { + "pendingTitle": "Fotos in Moderation", + "pendingSubtitle": "Schnell prüfen, bevor Gäste live gehen.", + "pendingCount": "{{count}} Fotos offen", + "pendingEmpty": "Aktuell warten keine Fotos auf Freigabe.", + "openModeration": "Moderation öffnen", + "recentTitle": "Neueste Uploads", + "recentSubtitle": "Halte Ausschau nach Highlight-Momenten der Gäste.", + "recentEmpty": "Noch keine neuen Uploads." + }, + "feedback": { + "title": "Wie läuft dein Event?", + "subtitle": "Feedback hilft uns, neue Features zu priorisieren.", + "positive": "Super Lauf!", + "neutral": "Läuft", + "negative": "Braucht Support", + "placeholder": "Optional: Lass uns wissen, was gut funktioniert oder wo du Unterstützung brauchst.", + "errorTitle": "Feedback konnte nicht gesendet werden.", + "authError": "Deine Session ist abgelaufen. Bitte melde dich erneut an.", + "genericError": "Feedback konnte nicht gesendet werden.", + "submit": "Feedback senden", + "submitted": "Danke!" + } + }, "collections": { "title": "Aufgabenvorlagen", "subtitle": "Durchstöbere kuratierte Vorlagen oder aktiviere sie für deine Events.", diff --git a/resources/js/admin/i18n/locales/en/auth.json b/resources/js/admin/i18n/locales/en/auth.json index 9cba72f..dc1005c 100644 --- a/resources/js/admin/i18n/locales/en/auth.json +++ b/resources/js/admin/i18n/locales/en/auth.json @@ -1,10 +1,21 @@ { "login": { "title": "Tenant Admin", - "lead": "Sign in with your Fotospiel account. We will redirect you to the secure OAuth login and bring you back to the admin dashboard afterwards.", - "cta": "Sign in with tenant account", - "loading": "Please wait …", + "badge": "Fotospiel Tenant Admin", + "hero_title": "Event control that feels at home.", + "hero_subtitle": "Switch between tenants, monitor live uploads, and share beautiful invites — all in one calm workspace.", + "features": [ + "Design QR invites and print-ready layouts that match your brand in minutes.", + "Coordinate tasks, emotions, and achievements for every event flow.", + "Stay confident on event day with dashboards, live stats, and instant moderation." + ], + "lead": "You will be redirected to our secure OAuth login and come right back afterwards.", + "panel_copy": "Sign in with your Fotospiel admin credentials to continue. We secure your account with OAuth 2.1 and tenant-aware permissions.", + "cta": "Continue with Fotospiel login", + "loading": "Signing you in …", + "oauth_error_title": "Login not possible right now", "oauth_error": "Sign-in failed: {{message}}", + "support": "Need access? Contact your tenant owner or email support@fotospiel.de — we're happy to help.", "appearance_label": "Appearance" } } diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index 6c00a8c..d5c2597 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -283,12 +283,12 @@ }, "logo": { "label": "Logo", - "hint": "PNG or SVG, max. 1 MB. Appears in the top right corner.", + "hint": "PNG, max. 1 MB. Appears in the top right corner.", "remove": "Remove logo" }, "preview": { "title": "Preview", - "hint": "Visual reference for colours and texts. Save to generate new PDFs/SVGs." + "hint": "Visual reference for colours and texts. Save to generate new PDFs/PNGs." }, "actions": { "save": "Save", @@ -299,7 +299,9 @@ "actionLabel": "Customize layout", "errors": { "logoTooLarge": "Logo must not exceed 1 MB.", - "noLayout": "Please select a layout." + "noLayout": "Please select a layout.", + "downloadFailed": "Download failed. Please try again.", + "printFailed": "Printing could not be started." }, "defaults": { "badgeLabel": "Digital guest box", @@ -310,7 +312,8 @@ "Scan the QR code", "Create your profile", "Share your photos" - ] + ], + "textBlock": "New text block – add your own notes here." } } }, @@ -354,24 +357,35 @@ "export": { "title": "Print & export", "description": "Download print-ready files or launch a test print right away.", + "mode": { + "standard": "Standard layout", + "advanced": "Advanced editor" + }, + "previewHint": "Save after making changes to regenerate the export files.", + "noLayoutPreview": "No preview available yet. Save your layout first.", "selectPlaceholder": "Select invite", "noInviteSelected": "Select an invite first to start downloads.", "noLayouts": "There are currently no layouts available for this invite.", "actions": { - "print": "Print now" + "title": "Actions", + "description": "Start a test print or download the layouts.", + "printNow": "Print now", + "hint": "PDF includes crop marks; PNG is perfect for quick digital approvals." }, "errorTitle": "Download failed" }, "customizer": { "heading": "Customise layout", - "copy": "Make the invite your own – adjust copy, colours, and logos in real time.", + "copy": "Edit texts, colours, and positions right next to the live preview. Changes appear instantly.", "actions": { "save": "Save layout", "reset": "Reset", "print": "Print", "removeLogo": "Remove logo", "uploadLogo": "Upload logo (max. 1 MB)", - "addInstruction": "Add step" + "addInstruction": "Add step", + "undo": "Undo", + "redo": "Redo" }, "sections": { "layouts": "Layouts", @@ -381,6 +395,37 @@ "instructionsHint": "Guide guests with clear steps. Maximum of five.", "branding": "Colors" }, + "elements": { + "title": "Elements & positioning", + "hint": "Select an element to move, edit, or remove it.", + "headline": "Headline", + "subtitle": "Subheading", + "description": "Description", + "badge": "Badge", + "link": "Link field", + "cta": "Call-to-action", + "qr": "QR code", + "logo": "Logo", + "text": "Free text block", + "remove": "Remove element", + "details": "Element details", + "detailsHint": "Adjust copy, alignment, and more. Changes appear instantly in the preview.", + "customText": "Text content", + "qrHint": "Resize and reposition the QR code directly on the canvas.", + "logoHint": "Adjust logos in the branding section. Use the canvas to resize or move the element.", + "align": "Alignment", + "alignLeft": "Left", + "alignCenter": "Center", + "alignRight": "Right", + "fontSize": "Font size", + "selectHint": "Tap an element on the canvas or in the list to edit its details.", + "listHint": "Select an element to reveal its settings directly under the entry.", + "addSubtitle": "Show subtitle", + "addBadge": "Show badge", + "addLink": "Add link field", + "addCta": "Insert call-to-action", + "addText": "Add free text block" + }, "fields": { "headline": "Headline", "subtitle": "Subheading", @@ -415,6 +460,113 @@ "layoutFallback": "Layout" } }, + "events": { + "errors": { + "missingSlug": "No event selected.", + "loadFailed": "Event could not be loaded.", + "notFoundTitle": "Event not found", + "notFoundBody": "Without a valid identifier we can’t load the data. Return to the list and choose an event.", + "toggleFailed": "Status could not be updated." + }, + "alerts": { + "failedTitle": "Action failed" + }, + "placeholders": { + "untitled": "Untitled event" + }, + "actions": { + "backToList": "Back to list", + "edit": "Edit", + "members": "Team & roles", + "tasks": "Manage tasks", + "invites": "Invites & layouts", + "photos": "Moderate photos", + "refresh": "Refresh" + }, + "workspace": { + "detailSubtitle": "Keep status, tasks, and invites of your event in one view.", + "toolkitSubtitle": "Bundle moderation, tasks, and invites for the event day.", + "sections": { + "statusTitle": "Event status & visibility", + "statusSubtitle": "Activate the event for guests or hide it temporarily." + }, + "fields": { + "status": "Status", + "active": "Active for guests", + "date": "Event date", + "eventType": "Event type", + "insights": "Recent activity", + "uploadsTotal": "{{count}} uploads total", + "uploadsToday": "{{count}} uploads (24h)", + "likesTotal": "{{count}} likes in total" + }, + "actions": { + "pause": "Pause event", + "activate": "Activate event" + }, + "activeYes": "Yes", + "activeNo": "No" + }, + "status": { + "published": "Published", + "draft": "Draft", + "archived": "Archived" + }, + "quickActions": { + "title": "Quick actions", + "subtitle": "Jump into the most important flows before and during the event.", + "moderate": "Moderate photos", + "tasks": "Edit tasks", + "invites": "Manage layouts & QR", + "roles": "Adjust team & roles", + "print": "Print layouts as PDF", + "toggle": "Change status" + }, + "metrics": { + "uploadsTotal": "Uploads total", + "uploads24h": "Uploads (24h)", + "pending": "Photos in moderation", + "activeInvites": "Active invites" + }, + "invites": { + "title": "QR invites", + "subtitle": "Keep an eye on active links and layouts.", + "activeCount": "{{count}} active", + "totalCount": "{{count}} total", + "empty": "No invites created yet.", + "manage": "Manage layouts & invites" + }, + "tasks": { + "title": "Active tasks", + "subtitle": "Motivate guests with clear prompts & highlights.", + "summary": "{{completed}} of {{total}} complete", + "empty": "No tasks assigned yet.", + "manage": "Open task workspace" + }, + "photos": { + "pendingTitle": "Photos awaiting review", + "pendingSubtitle": "Check uploads before they go live.", + "pendingCount": "{{count}} photos pending", + "pendingEmpty": "No photos waiting for moderation.", + "openModeration": "Open moderation", + "recentTitle": "Latest uploads", + "recentSubtitle": "Spot the latest guest highlights.", + "recentEmpty": "No new uploads yet." + }, + "feedback": { + "title": "How is your event running?", + "subtitle": "Your feedback helps us prioritise improvements.", + "positive": "Going great!", + "neutral": "All right", + "negative": "Needs support", + "placeholder": "Optional: tell us what works well or where you need help.", + "errorTitle": "Feedback could not be sent.", + "authError": "Your session expired. Please sign in again.", + "genericError": "Feedback could not be sent.", + "submit": "Send feedback", + "submitted": "Thanks!" + } + }, "collections": { "title": "Task collections", "subtitle": "Browse curated task bundles or activate them for your events.", diff --git a/resources/js/admin/pages/EventDetailPage.tsx b/resources/js/admin/pages/EventDetailPage.tsx index b68be04..decf73f 100644 --- a/resources/js/admin/pages/EventDetailPage.tsx +++ b/resources/js/admin/pages/EventDetailPage.tsx @@ -1,162 +1,195 @@ import React from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { ArrowLeft, Camera, Heart, Loader2, RefreshCw, Sparkles, QrCode } from 'lucide-react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + AlertTriangle, + ArrowLeft, + Camera, + CheckCircle2, + ChevronRight, + Circle, + Download, + Loader2, + MessageSquare, + Printer, + QrCode, + RefreshCw, + Smile, + Sparkles, + Users, +} from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { AdminLayout } from '../components/AdminLayout'; import { - EventStats as TenantEventStats, + EventToolkit, + EventToolkitTask, + TenantEvent, + TenantPhoto, + TenantEventStats, getEvent, getEventStats, - TenantEvent, + getEventToolkit, toggleEvent, + submitTenantFeedback, } from '../api'; import { isAuthError } from '../auth/tokens'; import { ADMIN_EVENTS_PATH, ADMIN_EVENT_EDIT_PATH, - ADMIN_EVENT_PHOTOS_PATH, - ADMIN_EVENT_MEMBERS_PATH, - ADMIN_EVENT_TASKS_PATH, - ADMIN_EVENT_TOOLKIT_PATH, ADMIN_EVENT_INVITES_PATH, + ADMIN_EVENT_MEMBERS_PATH, + ADMIN_EVENT_PHOTOS_PATH, + ADMIN_EVENT_TASKS_PATH, } from '../constants'; -interface State { +type EventDetailPageProps = { + mode?: 'detail' | 'toolkit'; +}; + +type ToolkitState = { + data: EventToolkit | null; + loading: boolean; + error: string | null; +}; + +type WorkspaceState = { event: TenantEvent | null; stats: TenantEventStats | null; - error: string | null; loading: boolean; busy: boolean; -} + error: string | null; +}; -export default function EventDetailPage() { - const params = useParams<{ slug?: string }>(); - const [searchParams] = useSearchParams(); - const slug = params.slug ?? searchParams.get('slug') ?? null; +export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProps): JSX.Element { + const { slug: slugParam } = useParams<{ slug?: string }>(); const navigate = useNavigate(); + const { t } = useTranslation('management'); - const [state, setState] = React.useState({ + const slug = slugParam ?? null; + + const [state, setState] = React.useState({ event: null, stats: null, - error: null, loading: true, busy: false, + error: null, }); + const [toolkit, setToolkit] = React.useState({ data: null, loading: true, error: null }); + const load = React.useCallback(async () => { if (!slug) { - setState((prev) => ({ ...prev, loading: false, error: 'Kein Event-Slug angegeben.' })); + setState({ event: null, stats: null, loading: false, busy: false, error: t('events.errors.missingSlug', 'Kein Event ausgewählt.') }); + setToolkit({ data: null, loading: false, error: null }); return; } setState((prev) => ({ ...prev, loading: true, error: null })); + setToolkit((prev) => ({ ...prev, loading: true, error: null })); + try { - const [eventData, statsData] = await Promise.all([ - getEvent(slug), - getEventStats(slug), - ]); - setState((prev) => ({ - ...prev, - event: eventData, - stats: statsData, - loading: false, - })); - } catch (err) { - if (isAuthError(err)) return; - setState((prev) => ({ ...prev, error: 'Event konnte nicht geladen werden.', loading: false })); + const [eventData, statsData] = await Promise.all([getEvent(slug), getEventStats(slug)]); + setState((prev) => ({ ...prev, event: eventData, stats: statsData, loading: false })); + } catch (error) { + if (!isAuthError(error)) { + setState((prev) => ({ ...prev, error: t('events.errors.loadFailed', 'Event konnte nicht geladen werden.'), loading: false })); + } } - }, [slug]); + + try { + const toolkitData = await getEventToolkit(slug); + setToolkit({ data: toolkitData, loading: false, error: null }); + } catch (error) { + if (!isAuthError(error)) { + setToolkit({ data: null, loading: false, error: t('toolkit.errors.loadFailed', 'Toolkit-Daten konnten nicht geladen werden.') }); + } + } + }, [slug, t]); React.useEffect(() => { - load(); + void load(); }, [load]); - async function handleToggle() { - if (!slug) return; + async function handleToggle(): Promise { + if (!slug) { + return; + } + setState((prev) => ({ ...prev, busy: true, error: null })); + try { const updated = await toggleEvent(slug); setState((prev) => ({ ...prev, - event: updated, - stats: prev.stats ? { ...prev.stats, status: updated.status, is_active: Boolean(updated.is_active) } : prev.stats, busy: false, + event: updated, + stats: prev.stats + ? { + ...prev.stats, + status: updated.status, + is_active: Boolean(updated.is_active), + } + : prev.stats, })); - } catch (err) { - if (!isAuthError(err)) { - setState((prev) => ({ ...prev, error: 'Status konnte nicht angepasst werden.', busy: false })); + } catch (error) { + if (!isAuthError(error)) { + setState((prev) => ({ ...prev, busy: false, error: t('events.errors.toggleFailed', 'Status konnte nicht angepasst werden.') })); } else { setState((prev) => ({ ...prev, busy: false })); } } } - const { event, stats, error, loading, busy } = state; - const eventDisplayName = event ? renderName(event.name) : ''; - const activeInvitesCount = event?.active_invites_count ?? 0; - const totalInvitesCount = event?.total_invites_count ?? activeInvitesCount; + const { event, stats, loading, busy, error } = state; + const toolkitData = toolkit.data; + + const eventName = event ? resolveName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event'); + const subtitle = mode === 'toolkit' + ? t('events.workspace.toolkitSubtitle', 'Moderation, Aufgaben und Einladungen für deinen Eventtag bündeln.') + : t('events.workspace.detailSubtitle', 'Behalte Status, Aufgaben und Einladungen deines Events im Blick.'); const actions = ( - <> - {event && ( <> - - - - - + )} - +

); if (!slug) { return ( - + - Ohne gültigen Slug können wir keine Daten laden. Kehre zur Event-Liste zurück und wähle dort ein Event aus. + {t('events.errors.notFoundBody', 'Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.')} @@ -164,159 +197,591 @@ export default function EventDetailPage() { } return ( - + {error && ( - Aktion fehlgeschlagen + {t('events.alerts.failedTitle', 'Aktion fehlgeschlagen')} {error} )} + {toolkit.error && ( + + {toolkit.error} + + )} + {loading ? ( - + ) : event ? ( -
- - - - Eventdaten - - - Grundlegende Informationen für Gäste und Moderation. - - - - - - - -
- - -
-
-
+
+ {(toolkitData?.alerts?.length ?? 0) > 0 && } - - - - Einladungen & QR - - - Steuere QR-Einladungen, Layouts und Branding gesammelt auf einer eigenen Seite. - - - -
-

- Aktive QR-Einladungen: {activeInvitesCount} · Gesamt erstellt: {totalInvitesCount} -

-

- Bereite deine Drucklayouts vor, personalisiere Texte und Logos und drucke sie direkt aus. -

-
+
+ + +
-
- -

- Du kannst bestehende Layouts duplizieren, Farben anpassen und neue PDFs generieren. -

-
-
-
+ - - - - Performance - - - Kennzahlen zu Uploads, Highlights und Interaktion. - - - - - - - - - +
+ navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} /> + navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)} + /> +
+ +
+ navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))} + /> + +
+ +
) : ( - - Event nicht gefunden - Bitte prüfe den Slug und versuche es erneut. - + + + {t('events.errors.notFoundBody', 'Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.')} + + )} ); } -function DetailSkeleton() { +function resolveName(name: TenantEvent['name']): string { + if (typeof name === 'string' && name.trim().length > 0) { + return name.trim(); + } + + if (name && typeof name === 'object') { + return name.de ?? name.en ?? Object.values(name)[0] ?? 'Event'; + } + + return 'Event'; +} + +function StatusCard({ event, stats, busy, onToggle }: { event: TenantEvent; stats: TenantEventStats | null; busy: boolean; onToggle: () => void }) { + const { t } = useTranslation('management'); + + const statusLabel = event.status === 'published' + ? t('events.status.published', 'Veröffentlicht') + : event.status === 'draft' + ? t('events.status.draft', 'Entwurf') + : t('events.status.archived', 'Archiviert'); + return ( -
- {Array.from({ length: 3 }).map((_, index) => ( -
+ + + + + {t('events.workspace.sections.statusTitle', 'Eventstatus & Sichtbarkeit')} + + + {t('events.workspace.sections.statusSubtitle', 'Aktiviere dein Event für Gäste oder verstecke es vorübergehend.')} + + + + } label={t('events.workspace.fields.status', 'Status')} value={statusLabel} /> + } label={t('events.workspace.fields.active', 'Aktiv für Gäste')} value={event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')} /> + } label={t('events.workspace.fields.date', 'Eventdatum')} value={formatDate(event.event_date)} /> + } label={t('events.workspace.fields.eventType', 'Event-Typ')} value={resolveEventType(event)} /> + + {stats && ( +
+

{t('events.workspace.fields.insights', 'Letzte Aktivität')}

+

+ {t('events.workspace.fields.uploadsTotal', { defaultValue: '{{count}} Uploads gesamt', count: stats.uploads_total })} + {' · '} + {t('events.workspace.fields.uploadsToday', { defaultValue: '{{count}} Uploads (24h)', count: stats.uploads_24h })} +

+

+ {t('events.workspace.fields.likesTotal', { defaultValue: '{{count}} Likes vergeben', count: stats.likes_total })} +

+
+ )} + +
+ +
+
+
+ ); +} + +function QuickActionsCard({ slug, busy, onToggle, navigate }: { slug: string; busy: boolean; onToggle: () => void | Promise; navigate: ReturnType }) { + const { t } = useTranslation('management'); + + const actions = [ + { + icon: , + label: t('events.quickActions.moderate', 'Fotos moderieren'), + onClick: () => navigate(ADMIN_EVENT_PHOTOS_PATH(slug)), + }, + { + icon: , + label: t('events.quickActions.tasks', 'Aufgaben bearbeiten'), + onClick: () => navigate(ADMIN_EVENT_TASKS_PATH(slug)), + }, + { + icon: , + label: t('events.quickActions.invites', 'Layouts & QR verwalten'), + onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=layout`), + }, + { + icon: , + label: t('events.quickActions.roles', 'Team & Rollen anpassen'), + onClick: () => navigate(ADMIN_EVENT_MEMBERS_PATH(slug)), + }, + { + icon: , + label: t('events.quickActions.print', 'Layouts als PDF drucken'), + onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=export`), + }, + { + icon: , + label: t('events.quickActions.toggle', 'Status ändern'), + onClick: () => { void onToggle(); }, + disabled: busy, + }, + ]; + + return ( + + + + + {t('events.quickActions.title', 'Schnellaktionen')} + + + {t('events.quickActions.subtitle', 'Nutze die wichtigsten Schritte vor und während deines Events.')} + + + + {actions.map((action, index) => ( + + ))} + + + ); +} + +function MetricsGrid({ metrics, stats }: { metrics: EventToolkit['metrics'] | null | undefined; stats: TenantEventStats | null }) { + const { t } = useTranslation('management'); + + const cards = [ + { + icon: , + label: t('events.metrics.uploadsTotal', 'Uploads gesamt'), + value: metrics?.uploads_total ?? stats?.uploads_total ?? 0, + }, + { + icon: , + label: t('events.metrics.uploads24h', 'Uploads (24h)'), + value: metrics?.uploads_24h ?? stats?.uploads_24h ?? 0, + }, + { + icon: , + label: t('events.metrics.pending', 'Fotos in Moderation'), + value: metrics?.pending_photos ?? stats?.pending_photos ?? 0, + }, + { + icon: , + label: t('events.metrics.activeInvites', 'Aktive Einladungen'), + value: metrics?.active_invites ?? 0, + }, + ]; + + return ( +
+ {cards.map((card, index) => ( + + + {card.icon} +
+

{card.label}

+

{card.value}

+
+
+
))}
); } -function InfoRow({ label, value }: { label: string; value: string }) { +function InviteSummary({ invites, navigateToInvites }: { invites: EventToolkit['invites'] | undefined; navigateToInvites: () => void }) { + const { t } = useTranslation('management'); + return ( -
- {label} - {value} + + + + + {t('events.invites.title', 'QR-Einladungen')} + + + {t('events.invites.subtitle', 'Behält aktive Einladungen und Layouts im Blick.')} + + + +
+ + {t('events.invites.activeCount', { defaultValue: '{{count}} aktiv', count: invites?.summary.active ?? 0 })} + + + {t('events.invites.totalCount', { defaultValue: '{{count}} gesamt', count: invites?.summary.total ?? 0 })} + +
+ + {invites?.items?.length ? ( +
    + {invites.items.slice(0, 3).map((invite) => ( +
  • +

    {invite.label ?? invite.url}

    +

    {invite.url}

    +
  • + ))} +
+ ) : ( +

{t('events.invites.empty', 'Noch keine Einladungen erstellt.')}

+ )} + + +
+
+ ); +} + +function TaskOverviewCard({ tasks, navigateToTasks }: { tasks: EventToolkit['tasks'] | undefined; navigateToTasks: () => void }) { + const { t } = useTranslation('management'); + + return ( + + +
+ + + {t('events.tasks.title', 'Aktive Aufgaben')} + + + {t('events.tasks.subtitle', 'Motiviere Gäste mit klaren Aufgaben & Highlights.')} + +
+ + {t('events.tasks.summary', { + defaultValue: '{{completed}} von {{total}} erledigt', + completed: tasks?.summary.completed ?? 0, + total: tasks?.summary.total ?? 0, + })} + +
+ + {tasks?.items?.length ? ( +
+ {tasks.items.slice(0, 4).map((task) => ( + + ))} +
+ ) : ( +

{t('events.tasks.empty', 'Noch keine Aufgaben zugewiesen.')}

+ )} + + +
+
+ ); +} + +function TaskRow({ task }: { task: EventToolkitTask }) { + return ( +
+
+

{task.title}

+ {task.description ?

{task.description}

: null} +
+ + {task.is_completed ? 'Erledigt' : 'Offen'} +
); } -function StatChip({ label, value }: { label: string; value: string | number }) { +function PendingPhotosCard({ photos, navigateToModeration }: { photos: TenantPhoto[]; navigateToModeration: () => void }) { + const { t } = useTranslation('management'); + return ( -
-
{label}
-
{value}
+ + +
+ + + {t('events.photos.pendingTitle', 'Fotos in Moderation')} + + + {t('events.photos.pendingSubtitle', 'Schnell prüfen, bevor Gäste live gehen.')} + +
+ + {t('events.photos.pendingCount', { defaultValue: '{{count}} Fotos offen', count: photos.length })} + +
+ + {photos.length ? ( +
+ {photos.slice(0, 6).map((photo) => ( + {photo.caption + ))} +
+ ) : ( +

{t('events.photos.pendingEmpty', 'Aktuell warten keine Fotos auf Freigabe.')}

+ )} + + +
+
+ ); +} + +function RecentUploadsCard({ photos }: { photos: TenantPhoto[] }) { + const { t } = useTranslation('management'); + + return ( + + + + + {t('events.photos.recentTitle', 'Neueste Uploads')} + + + {t('events.photos.recentSubtitle', 'Halte Ausschau nach Highlight-Momenten der Gäste.')} + + + + {photos.length ? ( +
+ {photos.slice(0, 6).map((photo) => ( + {photo.caption + ))} +
+ ) : ( +

{t('events.photos.recentEmpty', 'Noch keine neuen Uploads.')}

+ )} +
+
+ ); +} + +function FeedbackCard({ slug }: { slug: string }) { + const { t } = useTranslation('management'); + const [sentiment, setSentiment] = React.useState<'positive' | 'neutral' | 'negative' | null>(null); + const [message, setMessage] = React.useState(''); + const [busy, setBusy] = React.useState(false); + const [submitted, setSubmitted] = React.useState(false); + const [error, setError] = React.useState(null); + + const copy = { + positive: t('events.feedback.positive', 'Super Lauf!'), + neutral: t('events.feedback.neutral', 'Läuft'), + negative: t('events.feedback.negative', 'Braucht Support'), + }; + + return ( + + + + + {t('events.feedback.title', 'Wie läuft dein Event?')} + + + {t('events.feedback.subtitle', 'Feedback hilft uns, neue Features zu priorisieren.')} + + + + {error && ( + + {t('events.feedback.errorTitle', 'Feedback konnte nicht gesendet werden.')} + {error} + + )} + +
+ {(Object.keys(copy) as Array<'positive' | 'neutral' | 'negative'>).map((key) => ( + + ))} +
+ +