diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6efe1..d927b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## [1.7.7] - 2026-03-10 + +### Added +- Hexa pieces can now be exported as a print-ready A4 PDF from the Hexas list +- Choose tile size (Small/Medium/Large) before exporting; last-used size is remembered +- Tiles are arranged in a honeycomb pattern with crop marks for easy cutting +- Each Hexa has a configurable quantity field controlling how many copies appear in the PDF + ## [1.7.6] - 2026-03-08 ### Added diff --git a/app/Filament/Resources/HexaResource.php b/app/Filament/Resources/HexaResource.php index f6ecb25..f00aec8 100644 --- a/app/Filament/Resources/HexaResource.php +++ b/app/Filament/Resources/HexaResource.php @@ -59,6 +59,14 @@ public static function form(Form $form): Form ->maxLength(255) ->columnSpanFull(), + Forms\Components\TextInput::make('quantity') + ->label('Quantity') + ->numeric() + ->minValue(1) + ->default(1) + ->required() + ->columnSpanFull(), + Forms\Components\FileUpload::make('image') ->label('Image') ->image() @@ -104,6 +112,10 @@ public static function table(Table $table): Table ->badge() ->color('primary'), + Tables\Columns\TextColumn::make('quantity') + ->label('Qty') + ->sortable(), + Tables\Columns\TextColumn::make('description') ->label('Description') ->limit(50) diff --git a/app/Filament/Resources/HexaResource/Pages/ListHexas.php b/app/Filament/Resources/HexaResource/Pages/ListHexas.php index 0a214df..3f3151d 100644 --- a/app/Filament/Resources/HexaResource/Pages/ListHexas.php +++ b/app/Filament/Resources/HexaResource/Pages/ListHexas.php @@ -6,7 +6,9 @@ namespace App\Filament\Resources\HexaResource\Pages; use App\Filament\Resources\HexaResource; +use App\Models\Game; use Filament\Actions; +use Filament\Forms\Components\Select; use Filament\Resources\Pages\ListRecords; class ListHexas extends ListRecords @@ -16,6 +18,37 @@ class ListHexas extends ListRecords protected function getHeaderActions(): array { return [ + Actions\Action::make('exportPdf') + ->label('Export PDF') + ->icon('heroicon-o-printer') + ->color('warning') + ->form([ + Select::make('game_id') + ->label('Game') + ->options(fn () => Game::where('creator_id', auth()->id())->pluck('name', 'id')) + ->required() + ->searchable() + ->preload(), + + Select::make('size') + ->label('Hexa Size') + ->options([ + 'small' => 'Small (~25mm)', + 'medium' => 'Medium (~40mm)', + 'large' => 'Large (~60mm)', + ]) + ->default(session('hexa_export_size', 'medium')) + ->required(), + ]) + ->action(function (array $data) { + session(['hexa_export_size' => $data['size']]); + + return redirect()->route('hexa.export.pdf', [ + 'game_id' => $data['game_id'], + 'size' => $data['size'], + ]); + }), + Actions\CreateAction::make(), ]; } diff --git a/app/Http/Controllers/HexaExportController.php b/app/Http/Controllers/HexaExportController.php new file mode 100644 index 0000000..5ee4b16 --- /dev/null +++ b/app/Http/Controllers/HexaExportController.php @@ -0,0 +1,28 @@ +validate([ + 'game_id' => 'required|exists:games,id', + 'size' => 'required|in:small,medium,large', + ]); + + // Ensure the game belongs to the authenticated user + $game = Game::where('id', $request->game_id) + ->where('creator_id', auth()->id()) + ->firstOrFail(); + + return app(HexaPdfService::class)->download($game, $request->size); + } +} diff --git a/app/Models/Hexa.php b/app/Models/Hexa.php index 93f3954..a2a68cb 100644 --- a/app/Models/Hexa.php +++ b/app/Models/Hexa.php @@ -18,6 +18,7 @@ class Hexa extends Model 'name', 'description', 'image', + 'quantity', ]; public function game() diff --git a/app/Services/HexaPdfService.php b/app/Services/HexaPdfService.php new file mode 100644 index 0000000..ee23998 --- /dev/null +++ b/app/Services/HexaPdfService.php @@ -0,0 +1,209 @@ + ['ftf_mm' => 25, 'label' => 'Small (~25mm)'], + 'medium' => ['ftf_mm' => 40, 'label' => 'Medium (~40mm)'], + 'large' => ['ftf_mm' => 60, 'label' => 'Large (~60mm)'], + ]; + + const PAGE_W_PT = 595.28; + const PAGE_H_PT = 841.89; + const MM_TO_PT = 72 / 25.4; + + /** @return array */ + public function expandHexas(Game $game): array + { + $hexas = Hexa::where('game_id', $game->id)->orderBy('name')->get(); + $expanded = []; + foreach ($hexas as $hexa) { + $qty = max(1, (int) ($hexa->quantity ?? 1)); + for ($i = 0; $i < $qty; $i++) { + $expanded[] = $hexa; + } + } + return $expanded; + } + + protected function resolveImagePath(Hexa $hexa): ?string + { + if (empty($hexa->image)) { + return null; + } + $path = Storage::disk('public')->path($hexa->image); + return file_exists($path) ? $path : null; + } + + /** + * Flat-top hex vertices at angle 0°=right. + * Returns [[x,y], ...] in PDF points (top-down coords, dompdf handles y-flip internally). + */ + protected function hexVertices(float $cx, float $cy, float $r): array + { + $pts = []; + for ($i = 0; $i < 6; $i++) { + $angle = deg2rad(60 * $i); + $pts[] = [$cx + $r * cos($angle), $cy + $r * sin($angle)]; + } + return $pts; + } + + /** Flat [x0,y0,x1,y1,...] array for CPDF polygon() / clipping_polygon() */ + protected function hexPts(float $cx, float $cy, float $r): array + { + $flat = []; + foreach ($this->hexVertices($cx, $cy, $r) as [$vx, $vy]) { + $flat[] = $vx; + $flat[] = $vy; + } + return $flat; + } + + public function download(Game $game, string $size): \Illuminate\Http\Response + { + $sizeConfig = $this->sizes[$size] ?? $this->sizes['medium']; + $ftfMm = $sizeConfig['ftf_mm']; + $sizeLabel = $sizeConfig['label']; + + // Flat-top hex: flat-to-flat (y) = sqrt(3)*r, vertex-to-vertex (x) = 2*r + $rPt = ($ftfMm / sqrt(3)) * self::MM_TO_PT; + $hexWPt = 2 * $rPt; // bounding box width (x: vertex to vertex) + $hexHPt = sqrt(3) * $rPt; // bounding box height (y: flat to flat) + $marginPt = 8 * self::MM_TO_PT; + $headerHPt = 12 * self::MM_TO_PT; + $gapPt = 6 * self::MM_TO_PT; + + $usableW = self::PAGE_W_PT - 2 * $marginPt; + $colsPerRow = max(1, (int) floor(($usableW + 0.001) / ($hexWPt + $gapPt))); + $colStepPt = $hexWPt + $gapPt; + $rowStepPt = $hexHPt + $gapPt; + + $hexas = $this->expandHexas($game); + + // Compute tile positions per page + $tileLayout = []; + $col = $row = $rowsUsed = $pageIdx = 0; + + foreach ($hexas as $index => $hexa) { + $rowOffset = 0; + $tileBottom = $headerHPt + $marginPt + $rowsUsed * $rowStepPt + $hexHPt; + + if ($tileBottom > self::PAGE_H_PT - $marginPt && $col === 0 && $index > 0) { + $pageIdx++; + $rowsUsed = $row = 0; + $rowOffset = 0; + } + + $tileLayout[$pageIdx][] = [ + 'hexa' => $hexa, + 'img' => $this->resolveImagePath($hexa), + 'tileX' => $marginPt + $rowOffset + $col * $colStepPt, + 'tileY' => $headerHPt + $marginPt + $rowsUsed * $rowStepPt, + ]; + + $col++; + if ($col >= $colsPerRow) { + $col = 0; + $row++; + $rowsUsed++; + } + } + + // Build PDF + $dompdf = new Dompdf(); + $dompdf->loadHtml(''); + $dompdf->setPaper('a4', 'portrait'); + $dompdf->render(); + $canvas = $dompdf->getCanvas(); + + $font = $dompdf->getFontMetrics()->getFont('Helvetica'); + $fontSize = max(8.0, $ftfMm * 0.22); + + foreach ($tileLayout as $pIdx => $tiles) { + if ($pIdx > 0) { + $canvas->new_page(); + } + $this->drawHeader($canvas, $font, $game->name, $sizeLabel, $marginPt, $headerHPt, $fontSize); + + foreach ($tiles as $tile) { + $this->drawTile($canvas, $font, $tile['hexa'], $tile['img'], $tile['tileX'], $tile['tileY'], $hexWPt, $hexHPt, $rPt, $fontSize); + } + } + + $filename = 'hexas-' . str($game->name)->slug() . '-' . $size . '.pdf'; + + return response($canvas->output(), 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]); + } + + protected function drawHeader( + \Dompdf\Adapter\CPDF $canvas, + string $font, + string $gameName, + string $sizeLabel, + float $marginPt, + float $headerHPt, + float $fontSize + ): void { + $canvas->text($marginPt, $marginPt + 2, $gameName, $font, $fontSize + 2, [0, 0, 0]); + $canvas->text($marginPt, $marginPt + 2 + ($fontSize + 2) * 1.4, $sizeLabel, $font, $fontSize, [0.4, 0.4, 0.4]); + $canvas->line($marginPt, $headerHPt - 1, self::PAGE_W_PT - $marginPt, $headerHPt - 1, [0.7, 0.7, 0.7], 0.5); + } + + protected function drawTile( + \Dompdf\Adapter\CPDF $canvas, + string $font, + Hexa $hexa, + ?string $imagePath, + float $tileX, + float $tileY, + float $hexWPt, + float $hexHPt, + float $rPt, + float $fontSize + ): void { + $cx = $tileX + $hexWPt / 2; + $cy = $tileY + $hexHPt / 2; + $pts = $this->hexPts($cx, $cy, $rPt); + + // 1. Grey background fill + $canvas->polygon($pts, [0.82, 0.82, 0.82], null, [], true); + + // 2. Image clipped to hex shape via CPDF clipping path + if ($imagePath) { + $canvas->save(); + $canvas->clipping_polygon($pts); + $canvas->image($imagePath, $tileX, $tileY, $hexWPt, $hexHPt); + $canvas->clipping_end(); + $canvas->restore(); + } + + // 3. Outline on top + $canvas->polygon($pts, [0.15, 0.15, 0.15], 1.2, [], false); + + // 4. Centred name + $textW = $canvas->get_text_width($hexa->name, $font, $fontSize); + $canvas->text( + $cx - $textW / 2, + $cy - $fontSize * 0.35, + $hexa->name, + $font, + $fontSize, + $imagePath ? [1.0, 1.0, 1.0] : [0.1, 0.1, 0.1] + ); + } + +} diff --git a/config/app_version.php b/config/app_version.php index 4df6654..0f3258e 100644 --- a/config/app_version.php +++ b/config/app_version.php @@ -11,9 +11,9 @@ | */ - 'version' => env('APP_VERSION', 'v1.7.5'), + 'version' => env('APP_VERSION', 'v1.7.7'), - 'release_date' => '2026-03-08', + 'release_date' => '2026-03-10', 'status' => 'stable', // alpha, beta, stable diff --git a/database/migrations/2026_03_10_000001_add_quantity_to_hexas_table.php b/database/migrations/2026_03_10_000001_add_quantity_to_hexas_table.php new file mode 100644 index 0000000..84264bc --- /dev/null +++ b/database/migrations/2026_03_10_000001_add_quantity_to_hexas_table.php @@ -0,0 +1,25 @@ +unsignedInteger('quantity')->default(1)->after('image'); + }); + } + + public function down(): void + { + Schema::table('hexas', function (Blueprint $table) { + $table->dropColumn('quantity'); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index bcbdd01..e9949d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "npm": "^11.10.1" + "npm": "^11.11.0" }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", @@ -37,6 +37,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -238,6 +239,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -286,6 +288,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -302,6 +305,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -314,6 +318,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -338,6 +343,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -350,6 +356,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -694,12 +701,14 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -905,6 +914,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -914,6 +924,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -942,9 +953,9 @@ } }, "node_modules/npm": { - "version": "11.10.1", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.10.1.tgz", - "integrity": "sha512-woavuY2OgDFQ1K/tB9QHsUuW989nKfvsKTN/h5qGyS+3+BhvXN/DA2TNzx569JaFfTqrET5bEQNHwVhFk+U1gg==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.11.0.tgz", + "integrity": "sha512-82gRxKrh/eY5UnNorkTFcdBQAGpgjWehkfGVqAGlJjejEtJZGGJUqjo3mbBTNbc5BTnPKGVtGPBZGhElujX5cw==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -1022,7 +1033,7 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.3.1", + "@npmcli/arborist": "^9.4.0", "@npmcli/config": "^10.7.1", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", @@ -1047,16 +1058,16 @@ "is-cidr": "^6.0.3", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.2", - "libnpmexec": "^10.2.2", - "libnpmfund": "^7.0.16", + "libnpmdiff": "^8.1.3", + "libnpmexec": "^10.2.3", + "libnpmfund": "^7.0.17", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.2", + "libnpmpack": "^9.1.3", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", "libnpmversion": "^8.0.3", - "make-fetch-happen": "^15.0.3", + "make-fetch-happen": "^15.0.4", "minimatch": "^10.2.2", "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", @@ -1071,7 +1082,7 @@ "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", - "pacote": "^21.3.1", + "pacote": "^21.4.0", "parse-conflict-json": "^5.0.1", "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", @@ -1095,6 +1106,25 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/npm/node_modules/@gar/promise-retry": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@gar/promise-retry/node_modules/retry": { + "version": "0.13.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/npm/node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "inBundle": true, @@ -1127,7 +1157,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.3.1", + "version": "9.4.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -1202,16 +1232,16 @@ } }, "node_modules/npm/node_modules/@npmcli/git": { - "version": "7.0.1", + "version": "7.0.2", "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" }, @@ -1457,11 +1487,11 @@ "license": "MIT" }, "node_modules/npm/node_modules/balanced-match": { - "version": "4.0.3", + "version": "4.0.4", "inBundle": true, "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/npm/node_modules/bin-links": { @@ -1491,14 +1521,14 @@ } }, "node_modules/npm/node_modules/brace-expansion": { - "version": "5.0.2", + "version": "5.0.3", "inBundle": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/npm/node_modules/cacache": { @@ -1616,7 +1646,6 @@ }, "node_modules/npm/node_modules/encoding": { "version": "0.1.13", - "inBundle": true, "license": "MIT", "optional": true, "dependencies": { @@ -1722,7 +1751,7 @@ } }, "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", + "version": "0.7.2", "inBundle": true, "license": "MIT", "optional": true, @@ -1731,6 +1760,10 @@ }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/npm/node_modules/ignore-walk": { @@ -1850,11 +1883,11 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.1.2", + "version": "8.1.3", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.3.1", + "@npmcli/arborist": "^9.4.0", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -1868,18 +1901,18 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.2.2", + "version": "10.2.3", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.3.1", + "@gar/promise-retry": "^1.0.0", + "@npmcli/arborist": "^9.4.0", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "read": "^5.0.1", "semver": "^7.3.7", "signal-exit": "^4.1.0", @@ -1890,11 +1923,11 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.16", + "version": "7.0.17", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.3.1" + "@npmcli/arborist": "^9.4.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -1913,11 +1946,11 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.1.2", + "version": "9.1.3", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.3.1", + "@npmcli/arborist": "^9.4.0", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -1991,10 +2024,11 @@ } }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "15.0.3", + "version": "15.0.4", "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", @@ -2004,7 +2038,6 @@ "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "ssri": "^13.0.0" }, "engines": { @@ -2045,7 +2078,7 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "5.0.1", + "version": "5.0.2", "inBundle": true, "license": "MIT", "dependencies": { @@ -2057,7 +2090,7 @@ "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { - "encoding": "^0.1.13" + "iconv-lite": "^0.7.2" } }, "node_modules/npm/node_modules/minipass-flush": { @@ -2247,7 +2280,7 @@ } }, "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.3", + "version": "10.0.4", "inBundle": true, "license": "ISC", "dependencies": { @@ -2322,10 +2355,11 @@ } }, "node_modules/npm/node_modules/pacote": { - "version": "21.3.1", + "version": "21.4.0", "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", @@ -2339,7 +2373,6 @@ "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" @@ -2574,7 +2607,7 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.22", + "version": "3.0.23", "inBundle": true, "license": "CC0-1.0" }, @@ -2938,6 +2971,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -2974,6 +3008,7 @@ "version": "7.5.1", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -3151,6 +3186,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 9264bb7..3047139 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,6 @@ "vite": "^6.3.6" }, "dependencies": { - "npm": "^11.10.1" + "npm": "^11.11.0" } } diff --git a/routes/web.php b/routes/web.php index 5180289..55a6492 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,6 +4,7 @@ use App\Http\Controllers\HomeController; use App\Http\Controllers\ChangelogController; use App\Http\Controllers\DeckPdfController; +use App\Http\Controllers\HexaExportController; use App\Http\Controllers\MarketplaceController; use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Http\Request; @@ -41,6 +42,11 @@ ->middleware(['auth']) ->name('decks.pdf'); + // Hexa PDF export (auth-protected, serves binary outside Livewire) + Route::get('/hexas/export', [HexaExportController::class, 'export']) + ->middleware(['auth']) + ->name('hexa.export.pdf'); + // Email Verification Routes Route::get('/email/verify/{id}/{hash}', function (Request $request, $id, $hash) { $user = \App\Models\User::findOrFail($id);