diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2cde56..3fec243 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to this project will be documented in this file.
+## [1.5.0] - 2026-02-22
+
+### Added
+- Configurable background frame for printed card PDFs: supervisors set a system-wide default, users can upload a personal override in their profile, with automatic fallback to the built-in card back image
+
## [1.4.0] - 2026-02-21
### Added
diff --git a/app/Filament/Pages/PdfSettings.php b/app/Filament/Pages/PdfSettings.php
new file mode 100644
index 0000000..5f44699
--- /dev/null
+++ b/app/Filament/Pages/PdfSettings.php
@@ -0,0 +1,139 @@
+check() && auth()->user()->isSupervisor();
+ }
+
+ /**
+ * Load current settings into the form.
+ */
+ public function mount(): void
+ {
+ $this->form->fill([
+ 'card_pdf_background' => WebsiteSetting::get('card_pdf_background'),
+ 'card_pdf_overlay' => WebsiteSetting::get('card_pdf_overlay', 'dark'),
+ ]);
+ }
+
+ /**
+ * Define the settings form.
+ */
+ public function form(Form $form): Form
+ {
+ return $form
+ ->schema([
+ Section::make('Card Print Background')
+ ->description('Upload a default background image applied to all printed card PDFs. Users may override this with their own background in their profile.')
+ ->icon('heroicon-o-photo')
+ ->schema([
+ FileUpload::make('card_pdf_background')
+ ->label('Default Card Background Image')
+ ->disk('public')
+ ->directory('pdf-backgrounds')
+ ->image()
+ ->imageEditor()
+ ->maxSize(5120)
+ ->helperText('Recommended: match your card dimensions. Falls back to the built-in card back image if not set.')
+ ->columnSpanFull(),
+ Select::make('card_pdf_overlay')
+ ->label('Default Overlay Style')
+ ->options([
+ 'dark' => 'Dark — dark overlay, light text (best for light backgrounds)',
+ 'light' => 'Light — light overlay, dark text (best for light backgrounds)',
+ ])
+ ->default('dark')
+ ->required()
+ ->helperText('Controls the overlay tint and text colours applied over the card background. Decks can override this individually.')
+ ->columnSpanFull(),
+ ]),
+ ])
+ ->statePath('data');
+ }
+
+ /**
+ * Save the settings to the database.
+ */
+ public function save(): void
+ {
+ $data = $this->form->getState();
+
+ WebsiteSetting::updateOrCreate(
+ ['key' => 'card_pdf_background'],
+ [
+ 'value' => $data['card_pdf_background'] ?? null,
+ 'type' => 'text',
+ 'group' => 'general',
+ 'label' => 'Card PDF Background',
+ 'description' => 'System-wide default background image for printed card PDFs.',
+ 'order' => 1,
+ ]
+ );
+
+ WebsiteSetting::updateOrCreate(
+ ['key' => 'card_pdf_overlay'],
+ [
+ 'value' => $data['card_pdf_overlay'] ?? 'dark',
+ 'type' => 'text',
+ 'group' => 'general',
+ 'label' => 'Card PDF Overlay Style',
+ 'description' => 'System-wide default overlay style (dark or light) for printed card PDFs.',
+ 'order' => 2,
+ ]
+ );
+
+ Notification::make()
+ ->title('PDF settings saved successfully.')
+ ->success()
+ ->send();
+ }
+
+ /**
+ * Get the header actions.
+ */
+ protected function getHeaderActions(): array
+ {
+ return [
+ Action::make('save')
+ ->label('Save Settings')
+ ->icon('heroicon-o-check')
+ ->color('primary')
+ ->action('save'),
+ ];
+ }
+}
diff --git a/app/Filament/Resources/DeckResource.php b/app/Filament/Resources/DeckResource.php
index 1429452..3b1475f 100644
--- a/app/Filament/Resources/DeckResource.php
+++ b/app/Filament/Resources/DeckResource.php
@@ -8,6 +8,8 @@
use App\Models\Deck;
use App\Models\Game;
use Filament\Forms;
+use Filament\Forms\Components\FileUpload;
+use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
@@ -83,6 +85,30 @@ public static function form(Form $form): Form
->collapsible(),
])
->columns(2),
+ Forms\Components\Section::make('Print Settings')
+ ->description('Customize how this deck looks when printed as a PDF')
+ ->schema([
+ FileUpload::make('pdf_background')
+ ->label('Card Background Image')
+ ->disk('public')
+ ->directory('pdf-backgrounds')
+ ->image()
+ ->imageEditor()
+ ->maxSize(5120)
+ ->helperText('Overrides the system default. Appears behind each card illustration in the printed PDF.')
+ ->columnSpanFull(),
+ Select::make('pdf_overlay')
+ ->label('Overlay Style')
+ ->options([
+ 'dark' => 'Dark — dark overlay, light text',
+ 'light' => 'Light — light overlay, dark text',
+ ])
+ ->placeholder('Use system default')
+ ->helperText('Overrides the system default overlay style for this deck only.')
+ ->columnSpanFull(),
+ ])
+ ->collapsible(),
+
Forms\Components\Section::make('Cards in Deck')
->description('There is the list of cards on this deck')
->schema([
diff --git a/app/Models/Deck.php b/app/Models/Deck.php
index a378e7f..ca7ebb1 100644
--- a/app/Models/Deck.php
+++ b/app/Models/Deck.php
@@ -22,6 +22,8 @@ class Deck extends Model
'deck_name',
'deck_description',
'deck_data',
+ 'pdf_background',
+ 'pdf_overlay',
];
public function game()
diff --git a/app/Services/DeckPdfService.php b/app/Services/DeckPdfService.php
index 5a77dc0..5d526ba 100644
--- a/app/Services/DeckPdfService.php
+++ b/app/Services/DeckPdfService.php
@@ -7,6 +7,7 @@
use App\Models\Card;
use App\Models\Deck;
+use App\Models\WebsiteSetting;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Storage;
@@ -61,19 +62,38 @@ public function expandCards(Deck $deck): array
}
/**
- * Resolve the filesystem path for a card image, falling back to the placeholder.
+ * Resolve the background image for the card layer:
+ * - If the card has its own illustration, use that.
+ * - Otherwise fall through: deck override → system default → built-in placeholder.
*/
- protected function resolveImagePath(Card $card): string
+ protected function resolveImagePath(Card $card, Deck $deck): string
{
- $placeholder = public_path('images/cardsforge-back.png');
+ if (!empty($card->image)) {
+ $path = Storage::disk('public')->path($card->image);
+ if (file_exists($path)) {
+ return $path;
+ }
+ }
- if (empty($card->image)) {
- return $placeholder;
+ // 1. Deck override
+ if (!empty($deck->pdf_background)) {
+ $path = Storage::disk('public')->path($deck->pdf_background);
+ if (file_exists($path)) {
+ return $path;
+ }
}
- $path = Storage::disk('public')->path($card->image);
+ // 2. System default
+ $setting = WebsiteSetting::get('card_pdf_background');
+ if (!empty($setting)) {
+ $path = Storage::disk('public')->path($setting);
+ if (file_exists($path)) {
+ return $path;
+ }
+ }
- return file_exists($path) ? $path : $placeholder;
+ // 3. Built-in fallback
+ return public_path('images/cardsforge-back.png');
}
/**
@@ -88,13 +108,17 @@ public function download(Deck $deck): \Symfony\Component\HttpFoundation\Streamed
$cardWidth = $deck->game->card_width_mm ?? 63.5;
$cardHeight = $deck->game->card_height_mm ?? 88.9;
+ // Resolve overlay mode: deck → system default → 'dark'
+ $overlayMode = $deck->pdf_overlay
+ ?? WebsiteSetting::get('card_pdf_overlay', 'dark');
+
$pagesData = [];
foreach ($pages as $pageCards) {
$pageItems = [];
foreach ($pageCards as $card) {
$pageItems[] = [
'card' => $card,
- 'imagePath' => $this->resolveImagePath($card),
+ 'imagePath' => $this->resolveImagePath($card, $deck),
];
}
$pagesData[] = $pageItems;
@@ -105,6 +129,7 @@ public function download(Deck $deck): \Symfony\Component\HttpFoundation\Streamed
'pages' => $pagesData,
'cardWidth' => $cardWidth,
'cardHeight' => $cardHeight,
+ 'overlayMode' => $overlayMode,
])
->setPaper('a4', 'portrait')
->setOption('isHtml5ParserEnabled', true)
diff --git a/database/migrations/2026_02_22_000001_add_pdf_background_to_users_table.php b/database/migrations/2026_02_22_000001_add_pdf_background_to_users_table.php
new file mode 100644
index 0000000..18ef7da
--- /dev/null
+++ b/database/migrations/2026_02_22_000001_add_pdf_background_to_users_table.php
@@ -0,0 +1,25 @@
+string('pdf_background')->nullable()->after('avatar_url');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('pdf_background');
+ });
+ }
+};
diff --git a/database/migrations/2026_02_22_000002_move_pdf_background_from_users_to_decks_table.php b/database/migrations/2026_02_22_000002_move_pdf_background_from_users_to_decks_table.php
new file mode 100644
index 0000000..ccad342
--- /dev/null
+++ b/database/migrations/2026_02_22_000002_move_pdf_background_from_users_to_decks_table.php
@@ -0,0 +1,33 @@
+string('pdf_background')->nullable()->after('deck_description');
+ });
+
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('pdf_background');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->string('pdf_background')->nullable()->after('avatar_url');
+ });
+
+ Schema::table('decks', function (Blueprint $table) {
+ $table->dropColumn('pdf_background');
+ });
+ }
+};
diff --git a/database/migrations/2026_02_22_000003_add_pdf_overlay_to_decks_table.php b/database/migrations/2026_02_22_000003_add_pdf_overlay_to_decks_table.php
new file mode 100644
index 0000000..7dc3201
--- /dev/null
+++ b/database/migrations/2026_02_22_000003_add_pdf_overlay_to_decks_table.php
@@ -0,0 +1,25 @@
+string('pdf_overlay')->nullable()->default(null)->after('pdf_background');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('decks', function (Blueprint $table) {
+ $table->dropColumn('pdf_overlay');
+ });
+ }
+};
diff --git a/resources/views/filament/pages/pdf-settings.blade.php b/resources/views/filament/pages/pdf-settings.blade.php
new file mode 100644
index 0000000..d45ede8
--- /dev/null
+++ b/resources/views/filament/pages/pdf-settings.blade.php
@@ -0,0 +1,5 @@
+