From d40b87438d939fb6c4dee4b0983278abda7617d1 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Fri, 2 Jan 2026 16:57:30 +0000 Subject: [PATCH 01/10] fix: add render hook for Vite in AdminPanelProvider --- app/Providers/Filament/AdminPanelProvider.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 373a4be..81aa43d 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -10,6 +10,8 @@ use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; +use Filament\Support\Facades\FilamentView; +use Filament\View\PanelsRenderHook; use Filament\Widgets\AccountWidget; use Filament\Widgets\FilamentInfoWidget; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -56,4 +58,12 @@ class AdminPanelProvider extends PanelProvider Authenticate::class, ]); } + + public function boot(): void + { + FilamentView::registerRenderHook( + PanelsRenderHook::BODY_END, + fn (): string => \Illuminate\Support\Facades\Blade::render('@vite("resources/js/app.js")'), + ); + } } From 02884d4e2b182b1d5c4d1939e04d575a9e2e79ef Mon Sep 17 00:00:00 2001 From: jon brookes Date: Fri, 2 Jan 2026 18:59:24 +0000 Subject: [PATCH 02/10] feat: implement Spatie Media Library integration with CRUD operations and media management UI --- .../Resources/Entries/Schemas/EntryForm.php | 50 ++++++++++- .../Resources/Entries/Tables/EntriesTable.php | 6 ++ .../Resources/Media/MediaResource.php | 58 +++++++++++++ .../Resources/Media/Pages/CreateMedia.php | 61 +++++++++++++ .../Resources/Media/Pages/EditMedia.php | 72 ++++++++++++++++ .../Resources/Media/Pages/ListMedia.php | 19 ++++ .../Resources/Media/Pages/ViewMedia.php | 19 ++++ .../Resources/Media/Schemas/MediaForm.php | 59 +++++++++++++ .../Resources/Media/Schemas/MediaInfolist.php | 36 ++++++++ .../Resources/Media/Tables/MediaTable.php | 86 +++++++++++++++++++ resources/js/app.js | 37 ++++++++ 11 files changed, 499 insertions(+), 4 deletions(-) create mode 100644 app/Filament/Resources/Media/MediaResource.php create mode 100644 app/Filament/Resources/Media/Pages/CreateMedia.php create mode 100644 app/Filament/Resources/Media/Pages/EditMedia.php create mode 100644 app/Filament/Resources/Media/Pages/ListMedia.php create mode 100644 app/Filament/Resources/Media/Pages/ViewMedia.php create mode 100644 app/Filament/Resources/Media/Schemas/MediaForm.php create mode 100644 app/Filament/Resources/Media/Schemas/MediaInfolist.php create mode 100644 app/Filament/Resources/Media/Tables/MediaTable.php diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index 82e163d..89c81b4 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -2,13 +2,16 @@ namespace App\Filament\Resources\Entries\Schemas; +use Filament\Actions\Action; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\RichEditor; -use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Schemas\Schema; use Illuminate\Support\Str; +use Spatie\MediaLibrary\MediaCollections\Models\Media; class EntryForm { @@ -18,13 +21,14 @@ class EntryForm ->components([ TextInput::make('title') ->required() - ->reactive() + ->live(onBlur: true) ->afterStateUpdated(function ($state, $set): void { $set('slug', Str::slug((string) $state)); }), TextInput::make('slug') ->required() - ->disabled(), + ->dehydrated() + ->readOnly(), Textarea::make('description') ->columnSpanFull(), Toggle::make('is_published') @@ -33,7 +37,45 @@ class EntryForm ->required(), DatePicker::make('published_at'), RichEditor::make('content') - ->columnSpanFull(), + ->columnSpanFull() + ->hintAction( + Action::make('picker') + ->label('Gallery Picker') + ->icon('heroicon-m-photo') + ->schema([ + Select::make('image_url') + ->label('Select an existing image') + ->allowHtml() + ->options(function () { + // We must 'get' the collection first so we can call getUrl() + // because 'url' is not a column in the Spatie database table. + return Media::latest() + ->get() + ->mapWithKeys(function (Media $item) { + $url = $item->getUrl(); + + $fileName = e($item->file_name); + $name = e($item->name ?? ''); + + $html = "
". + "\"{$fileName}\"/". + "{$name} — {$fileName}
"; + + return [$url => $html]; + })->toArray(); + }) + ->searchable() + ->required(), + ]) + ->action(function (array $data, RichEditor $component) { + // We dispatch the URL to the browser to be inserted into TipTap + $component->getLivewire()->dispatch('insert-editor-content', [ + 'statePath' => $component->getStatePath(), + 'html' => "", + ]); + }) + ), + ]); } } diff --git a/app/Filament/Resources/Entries/Tables/EntriesTable.php b/app/Filament/Resources/Entries/Tables/EntriesTable.php index 57035c0..4985529 100644 --- a/app/Filament/Resources/Entries/Tables/EntriesTable.php +++ b/app/Filament/Resources/Entries/Tables/EntriesTable.php @@ -7,6 +7,7 @@ use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Actions\ViewAction; use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -16,6 +17,11 @@ class EntriesTable { return $table ->columns([ + SpatieMediaLibraryImageColumn::make('featured_image') + ->collection('featured') + ->circular() + ->stacked() + ->limit(3), TextColumn::make('title') ->searchable(), TextColumn::make('slug') diff --git a/app/Filament/Resources/Media/MediaResource.php b/app/Filament/Resources/Media/MediaResource.php new file mode 100644 index 0000000..2ac19fe --- /dev/null +++ b/app/Filament/Resources/Media/MediaResource.php @@ -0,0 +1,58 @@ + ListMedia::route('/'), + 'create' => CreateMedia::route('/create'), + 'view' => ViewMedia::route('/{record}'), + 'edit' => EditMedia::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Media/Pages/CreateMedia.php b/app/Filament/Resources/Media/Pages/CreateMedia.php new file mode 100644 index 0000000..c652a96 --- /dev/null +++ b/app/Filament/Resources/Media/Pages/CreateMedia.php @@ -0,0 +1,61 @@ +uploadedFile = $file; + + // Set required fields for Media model + $data['model_type'] = $data['model_type'] ?? 'temp'; + $data['model_id'] = $data['model_id'] ?? 0; + $data['collection_name'] = $data['collection_name'] ?? 'default'; + $data['disk'] = $data['disk'] ?? 'public'; + $data['file_name'] = $file ? basename($file) : ''; + $data['mime_type'] = $file && Storage::disk('public')->exists($file) + ? Storage::disk('public')->mimeType($file) + : 'application/octet-stream'; + $data['size'] = $file && Storage::disk('public')->exists($file) + ? Storage::disk('public')->size($file) + : 0; + $data['manipulations'] = []; + $data['custom_properties'] = []; + $data['generated_conversions'] = []; + $data['responsive_images'] = []; + + return $data; + } + + protected function afterCreate(): void + { + if ($this->uploadedFile && $this->record) { + $disk = Storage::disk('public'); + + // Create the directory for this media ID (Spatie structure: {id}/{filename}) + $mediaDirectory = (string) $this->record->id; + $disk->makeDirectory($mediaDirectory); + + // Move file from temporary upload location to Spatie's expected location + if ($disk->exists($this->uploadedFile)) { + $newPath = $mediaDirectory.'/'.$this->record->file_name; + $disk->move($this->uploadedFile, $newPath); + } + } + } +} diff --git a/app/Filament/Resources/Media/Pages/EditMedia.php b/app/Filament/Resources/Media/Pages/EditMedia.php new file mode 100644 index 0000000..b6f4cdb --- /dev/null +++ b/app/Filament/Resources/Media/Pages/EditMedia.php @@ -0,0 +1,72 @@ +record->getPathRelativeToRoot()) { + $this->uploadedFile = $file; + + // Keep the original file_name to prevent breaking existing references + // $data['file_name'] is not updated - we preserve the original filename + $data['mime_type'] = Storage::disk('public')->exists($file) + ? Storage::disk('public')->mimeType($file) + : 'application/octet-stream'; + $data['size'] = Storage::disk('public')->exists($file) + ? Storage::disk('public')->size($file) + : 0; + } + + return $data; + } + + protected function afterSave(): void + { + if ($this->uploadedFile && $this->record) { + $disk = Storage::disk('public'); + $mediaDirectory = (string) $this->record->id; + + // Delete old file if it exists + $oldPath = $mediaDirectory.'/'.$this->record->getOriginal('file_name'); + if ($disk->exists($oldPath)) { + $disk->delete($oldPath); + } + + // Move new file to Spatie's expected location using the original filename + if ($disk->exists($this->uploadedFile)) { + $disk->makeDirectory($mediaDirectory); + // Use the original file_name to preserve existing references + $newPath = $mediaDirectory.'/'.$this->record->file_name; + $disk->move($this->uploadedFile, $newPath); + } + + // Redirect to the same page to refresh the form state + $this->redirect(static::getUrl(['record' => $this->record]), navigate: true); + } + } +} diff --git a/app/Filament/Resources/Media/Pages/ListMedia.php b/app/Filament/Resources/Media/Pages/ListMedia.php new file mode 100644 index 0000000..2e6b63d --- /dev/null +++ b/app/Filament/Resources/Media/Pages/ListMedia.php @@ -0,0 +1,19 @@ +components([ + TextInput::make('name') + ->required() + ->maxLength(255), + TextInput::make('collection_name') + ->default('default') + ->required() + ->maxLength(255), + Hidden::make('disk') + ->default('public'), + FileUpload::make('file') + ->label('File') + ->imageEditor() + ->imageEditorAspectRatios([ + '16:9', + '4:3', + '1:1', + ]) + ->columnSpanFull() + ->disk('public') + ->directory('media') + ->visibility('public') + ->acceptedFileTypes(['image/*', 'application/pdf']) + ->maxSize(10240) + ->required(fn ($context) => $context === 'create') + ->afterStateHydrated(function (FileUpload $component, $state, $record): void { + if (! $record) { + return; + } + + $media = $record; + + if (! $media instanceof SpatieMedia) { + return; + } + + // Construct the correct path: {media_id}/{filename} + $path = $media->id.'/'.$media->file_name; + + $component->state($path); + }), + ]); + } +} diff --git a/app/Filament/Resources/Media/Schemas/MediaInfolist.php b/app/Filament/Resources/Media/Schemas/MediaInfolist.php new file mode 100644 index 0000000..dfe4801 --- /dev/null +++ b/app/Filament/Resources/Media/Schemas/MediaInfolist.php @@ -0,0 +1,36 @@ +components([ + ImageEntry::make('file_name') + ->label('Preview') + ->getStateUsing(fn ($record) => $record->getUrl()) + ->visible(fn ($record) => $record->mime_type && str_starts_with($record->mime_type, 'image/')), + TextEntry::make('name'), + TextEntry::make('file_name'), + TextEntry::make('mime_type'), + TextEntry::make('collection_name'), + TextEntry::make('size') + ->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'), + TextEntry::make('model_type') + ->label('Attached to Model'), + TextEntry::make('model_id'), + TextEntry::make('custom_properties') + ->formatStateUsing(fn ($state) => json_encode($state, JSON_PRETTY_PRINT)), + TextEntry::make('created_at') + ->dateTime(), + TextEntry::make('updated_at') + ->dateTime(), + ]); + } +} diff --git a/app/Filament/Resources/Media/Tables/MediaTable.php b/app/Filament/Resources/Media/Tables/MediaTable.php new file mode 100644 index 0000000..dba76b0 --- /dev/null +++ b/app/Filament/Resources/Media/Tables/MediaTable.php @@ -0,0 +1,86 @@ +modifyQueryUsing(fn ($query) => $query->where('collection_name', '!=', 'avatars')) + ->columns([ + ImageColumn::make('url') + ->label('Preview') + ->getStateUsing(fn ($record) => + // Prefer the stored path produced by Filament's FileUpload (saved in custom_properties), + // fall back to Spatie's getUrl() when no stored_path exists. + ($record->getCustomProperty('stored_path')) + ? Storage::url($record->getCustomProperty('stored_path')) + : $record->getUrl() + ) + ->height(40) + ->width(40), + TextColumn::make('name') + ->searchable(), + TextColumn::make('file_name') + ->searchable(), + TextColumn::make('collection_name') + ->badge(), + TextColumn::make('mime_type'), + TextColumn::make('size') + ->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'), + TextColumn::make('created_at') + ->dateTime(), + ]) + ->filters([ + SelectFilter::make('collection_name') + ->options([ + 'images' => 'Images', + 'documents' => 'Documents', + ]), + ]) + ->recordActions([ + EditAction::make(), + DeleteAction::make() + ->action(function (Media $record) { + // Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path. + $stored = $record->getCustomProperty('stored_path'); + if ($stored) { + Storage::disk($record->disk)->delete($stored); + } else { + Storage::disk($record->disk)->delete($record->getPath()); + } + + $record->delete(); + }), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make() + ->action(function (Collection $records) { + $records->each(function (Media $record) { + $stored = $record->getCustomProperty('stored_path'); + if ($stored) { + Storage::disk($record->disk)->delete($stored); + } else { + Storage::disk($record->disk)->delete($record->getPath()); + } + $record->delete(); + }); + }), + ]), + ]); + } +} diff --git a/resources/js/app.js b/resources/js/app.js index b677733..b52c9ea 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,38 @@ console.log('App.js is loaded'); + +document.addEventListener('livewire:init', () => { + Livewire.on('insert-editor-content', (data) => { + console.log('Received insert-editor-content data:', data); + // Handle if data is an array + const payload = Array.isArray(data) ? data[0] : data; + const statePath = payload.statePath; + const html = payload.html; + console.log('Extracted statePath:', statePath, 'html:', html); + // 1. Find the editor by its statePath + const container = document.querySelector(`[wire\\:model="${statePath}"]`) || document.querySelector(`[statepath="${statePath}"]`); + console.log('Container found:', container); + const editorElement = container ? container.querySelector('.tiptap') : null; + console.log('Editor element found:', editorElement); + + if (editorElement && editorElement.editor) { + console.log('Inserting content:', html); + // 2. Insert the HTML (the tag) into the editor + setTimeout(() => { + editorElement.editor.chain().focus().insertContent(html).run(); + }, 500); + } else { + console.log('Editor not found or not initialized'); + // Fallback: try to find any .tiptap on the page + const anyTiptap = document.querySelector('.tiptap'); + console.log('Any tiptap found:', anyTiptap); + if (anyTiptap && anyTiptap.editor) { + console.log('Inserting to any tiptap'); + setTimeout(() => { + anyTiptap.editor.chain().focus().insertContent(html).run(); + }, 500); + } + } + }); +}); + +console.log('Testing if app.js is still running'); \ No newline at end of file From d24b9b07327cafaa51d99dd41ce1e8513ac34912 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 12:59:56 +0000 Subject: [PATCH 03/10] removed: filament/spatie-laravel-media-library-plugin --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 4745dfb..c67e0d1 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,6 @@ "require": { "php": "^8.2", "filament/filament": "^4.0", - "filament/spatie-laravel-media-library-plugin": "^4.0", "laravel/fortify": "^1.30", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", From 9f01d44c9dd0ecde41224af231768479ab52b1fc Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 13:22:14 +0000 Subject: [PATCH 04/10] added feaured image to entry edit form now has image upload for featured image table view for entries shows featured image view entry shows featured image --- app/Filament/Resources/Entries/Schemas/EntryForm.php | 6 ++++++ app/Filament/Resources/Entries/Schemas/EntryInfolist.php | 4 ++++ app/Filament/Resources/Entries/Tables/EntriesTable.php | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index 89c81b4..41c94e5 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -6,6 +6,7 @@ use Filament\Actions\Action; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\Select; +use Filament\Forms\Components\SpatieMediaLibraryFileUpload; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; @@ -31,6 +32,11 @@ class EntryForm ->readOnly(), Textarea::make('description') ->columnSpanFull(), + SpatieMediaLibraryFileUpload::make('featured_image') + ->collection('featured-image') + ->image() + ->imageEditor() + ->columnSpanFull(), Toggle::make('is_published') ->required(), Toggle::make('is_featured') diff --git a/app/Filament/Resources/Entries/Schemas/EntryInfolist.php b/app/Filament/Resources/Entries/Schemas/EntryInfolist.php index 6a5d87d..be741bc 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryInfolist.php +++ b/app/Filament/Resources/Entries/Schemas/EntryInfolist.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources\Entries\Schemas; use Filament\Infolists\Components\IconEntry; +use Filament\Infolists\Components\SpatieMediaLibraryImageEntry; use Filament\Infolists\Components\TextEntry; use Filament\Schemas\Schema; @@ -17,6 +18,9 @@ class EntryInfolist TextEntry::make('description') ->placeholder('-') ->columnSpanFull(), + SpatieMediaLibraryImageEntry::make('featured_image') + ->collection('featured-image') + ->columnSpanFull(), IconEntry::make('is_published') ->boolean(), IconEntry::make('is_featured') diff --git a/app/Filament/Resources/Entries/Tables/EntriesTable.php b/app/Filament/Resources/Entries/Tables/EntriesTable.php index 4985529..eb89fd4 100644 --- a/app/Filament/Resources/Entries/Tables/EntriesTable.php +++ b/app/Filament/Resources/Entries/Tables/EntriesTable.php @@ -18,7 +18,7 @@ class EntriesTable return $table ->columns([ SpatieMediaLibraryImageColumn::make('featured_image') - ->collection('featured') + ->collection('featured-image') ->circular() ->stacked() ->limit(3), From 340b466aded2ee95f7fa732e02c937987b1d44a0 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 13:35:38 +0000 Subject: [PATCH 05/10] feat: enhance featured image upload with gallery selection and preview --- .../Resources/Entries/Schemas/EntryForm.php | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index 41c94e5..a0613d3 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -36,7 +36,56 @@ class EntryForm ->collection('featured-image') ->image() ->imageEditor() - ->columnSpanFull(), + ->columnSpanFull() + ->hintAction( + Action::make('featured_picker') + ->label('Pick from Gallery') + ->icon('heroicon-m-photo') + ->schema([ + Select::make('image_id') + ->label('Select an existing image') + ->allowHtml() + ->options(function () { + return Media::latest() + ->get() + ->mapWithKeys(function (Media $item) { + $url = $item->getUrl(); + + $fileName = e($item->file_name); + $name = e($item->name ?? ''); + + $html = "
". + "\"{$fileName}\"/". + "{$name} — {$fileName}
"; + + return [$item->id => $html]; + })->toArray(); + }) + ->searchable() + ->required(), + ]) + ->action(function (array $data, SpatieMediaLibraryFileUpload $component) { + $record = $component->getRecord(); + + if ($record && $mediaItem = Media::find($data['image_id'])) { + // Clear any existing featured image + $record->clearMediaCollection('featured-image'); + + // Get the full path to the media file + $fullPath = $mediaItem->getPath(); + + // Add a copy of the media file to the featured-image collection + $record->addMedia($fullPath) + ->usingName($mediaItem->name ?: $mediaItem->file_name) + ->usingFileName($mediaItem->file_name) + ->preservingOriginal() + ->toMediaCollection('featured-image'); + + // Refresh the page to show the new featured image + $component->getLivewire()->dispatch('$refresh'); + } + }) + ), Toggle::make('is_published') ->required(), Toggle::make('is_featured') From c49249ee20d33ac545ab2bf8f5fbcab593460c95 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 14:25:23 +0000 Subject: [PATCH 06/10] fix: partical fix for featured imaes are added, seem semi permanent disapapar in media lib --- .../Resources/Entries/Schemas/EntryForm.php | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index a0613d3..739fe50 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -11,6 +11,7 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Schemas\Schema; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Spatie\MediaLibrary\MediaCollections\Models\Media; @@ -67,22 +68,33 @@ class EntryForm ->action(function (array $data, SpatieMediaLibraryFileUpload $component) { $record = $component->getRecord(); - if ($record && $mediaItem = Media::find($data['image_id'])) { - // Clear any existing featured image + if (!$record) { + \Filament\Notifications\Notification::make() + ->warning() + ->title('Save the entry first') + ->send(); + return; + } + + if ($mediaItem = Media::find($data['image_id'])) { + // Clear existing featured image $record->clearMediaCollection('featured-image'); - // Get the full path to the media file - $fullPath = $mediaItem->getPath(); + // Download from the full URL and add as new media + $fullUrl = url($mediaItem->getUrl()); - // Add a copy of the media file to the featured-image collection - $record->addMedia($fullPath) + $newMedia = $record->addMediaFromUrl($fullUrl) ->usingName($mediaItem->name ?: $mediaItem->file_name) ->usingFileName($mediaItem->file_name) - ->preservingOriginal() ->toMediaCollection('featured-image'); - // Refresh the page to show the new featured image - $component->getLivewire()->dispatch('$refresh'); + // Update component state + $component->state([$newMedia->uuid]); + + \Filament\Notifications\Notification::make() + ->success() + ->title('Featured image set') + ->send(); } }) ), From a94a34ce3b0a81ec228fb65307c40d9b81232210 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 14:52:04 +0000 Subject: [PATCH 07/10] fix: storage to public this was breaking things bad --- app/Console/Commands/MoveMediaToPublic.php | 112 +++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 app/Console/Commands/MoveMediaToPublic.php diff --git a/app/Console/Commands/MoveMediaToPublic.php b/app/Console/Commands/MoveMediaToPublic.php new file mode 100644 index 0000000..bce2727 --- /dev/null +++ b/app/Console/Commands/MoveMediaToPublic.php @@ -0,0 +1,112 @@ +option('dry-run'); + + if ($dryRun) { + $this->warn('DRY RUN MODE - No files will actually be moved'); + } + + // Get all media records using local disk + $mediaRecords = Media::where('disk', 'local')->get(); + + if ($mediaRecords->isEmpty()) { + $this->info('No media records found using local disk.'); + return self::SUCCESS; + } + + $this->info("Found {$mediaRecords->count()} media records to migrate."); + + $progressBar = $this->output->createProgressBar($mediaRecords->count()); + $progressBar->start(); + + $moved = 0; + $errors = 0; + + foreach ($mediaRecords as $media) { + // Use relative path: {id}/{filename} + $relativePath = $media->id . '/' . $media->file_name; + + // Check if source file exists + if (!Storage::disk('local')->exists($relativePath)) { + $this->newLine(); + $this->error("Source file not found: {$relativePath}"); + $errors++; + $progressBar->advance(); + continue; + } + + try { + if (!$dryRun) { + // Copy file from local to public disk + $fileContent = Storage::disk('local')->get($relativePath); + Storage::disk('public')->put($relativePath, $fileContent); + + // Verify the file was copied successfully + if (Storage::disk('public')->exists($relativePath)) { + // Update the database record + $media->update([ + 'disk' => 'public', + 'conversions_disk' => 'public', + ]); + + // Delete the old file from local disk + Storage::disk('local')->delete($relativePath); + + $moved++; + } else { + throw new \Exception("Failed to copy file to public disk"); + } + } else { + $this->newLine(); + $this->line("Would move: local:{$relativePath} -> public:{$relativePath}"); + $moved++; + } + } catch (\Exception $e) { + $this->newLine(); + $this->error("Error moving {$relativePath}: {$e->getMessage()}"); + $errors++; + } + + $progressBar->advance(); + } + + $progressBar->finish(); + $this->newLine(2); + + if ($dryRun) { + $this->info("DRY RUN: Would move {$moved} files, {$errors} errors encountered."); + $this->info("Run without --dry-run to actually perform the migration."); + } else { + $this->info("Successfully moved {$moved} files, {$errors} errors encountered."); + } + + return self::SUCCESS; + } +} From 6d1d88542e72aa0a9dc2248a92f297a0cae0e338 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 14:52:52 +0000 Subject: [PATCH 08/10] fix: storage to public private was breaking things bad --- app/Filament/Resources/Entries/Schemas/EntryForm.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index 739fe50..43cc948 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -37,6 +37,8 @@ class EntryForm ->collection('featured-image') ->image() ->imageEditor() + ->disk('public') + ->visibility('public') ->columnSpanFull() ->hintAction( Action::make('featured_picker') @@ -86,7 +88,7 @@ class EntryForm $newMedia = $record->addMediaFromUrl($fullUrl) ->usingName($mediaItem->name ?: $mediaItem->file_name) ->usingFileName($mediaItem->file_name) - ->toMediaCollection('featured-image'); + ->toMediaCollection('featured-image', 'public'); // Update component state $component->state([$newMedia->uuid]); From 8e1650653b9e12375eb8870f383509b17ce3d1f6 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 15:12:34 +0000 Subject: [PATCH 09/10] fix: partical images broken public seems to be holding still issues with picking images - not saved --- .../Resources/Entries/Schemas/EntryForm.php | 75 +++++++++++++------ 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index 43cc948..02b5aee 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -50,21 +50,36 @@ class EntryForm ->allowHtml() ->options(function () { return Media::latest() - ->get() + ->limit(30) // Limit to 30 most recent items for performance + ->get(['id', 'file_name', 'name', 'uuid', 'collection_name', 'model_type', 'model_id', 'disk']) + ->filter(function (Media $item) { + // Only include media items that have a valid disk + return $item->disk !== null; + }) ->mapWithKeys(function (Media $item) { - $url = $item->getUrl(); - + try { + $url = $item->getUrl(); + } catch (\Exception $e) { + // Skip items that can't generate URLs + return []; + } + $fileName = e($item->file_name); $name = e($item->name ?? ''); - - $html = "
". - "\"{$fileName}\"/". - "{$name} — {$fileName}
"; + + // Smaller image preview for better performance + $html = "
" . + "{$fileName}" . + "
" . + "{$name}" . + "{$fileName}" . + "
"; return [$item->id => $html]; })->toArray(); }) ->searchable() + ->preload() ->required(), ]) ->action(function (array $data, SpatieMediaLibraryFileUpload $component) { @@ -82,16 +97,15 @@ class EntryForm // Clear existing featured image $record->clearMediaCollection('featured-image'); - // Download from the full URL and add as new media - $fullUrl = url($mediaItem->getUrl()); - - $newMedia = $record->addMediaFromUrl($fullUrl) - ->usingName($mediaItem->name ?: $mediaItem->file_name) - ->usingFileName($mediaItem->file_name) - ->toMediaCollection('featured-image', 'public'); + // Associate the existing media with this entry instead of copying + $mediaItem->update([ + 'model_type' => get_class($record), + 'model_id' => $record->id, + 'collection_name' => 'featured-image' + ]); // Update component state - $component->state([$newMedia->uuid]); + $component->state([$mediaItem->uuid]); \Filament\Notifications\Notification::make() ->success() @@ -116,24 +130,37 @@ class EntryForm ->label('Select an existing image') ->allowHtml() ->options(function () { - // We must 'get' the collection first so we can call getUrl() - // because 'url' is not a column in the Spatie database table. return Media::latest() - ->get() + ->limit(30) // Limit to 30 most recent items for performance + ->get(['id', 'file_name', 'name', 'uuid', 'collection_name', 'model_type', 'model_id', 'disk']) + ->filter(function (Media $item) { + // Only include media items that have a valid disk + return $item->disk !== null; + }) ->mapWithKeys(function (Media $item) { - $url = $item->getUrl(); - + try { + $url = $item->getUrl(); + } catch (\Exception $e) { + // Skip items that can't generate URLs + return []; + } + $fileName = e($item->file_name); $name = e($item->name ?? ''); - - $html = "
". - "\"{$fileName}\"/". - "{$name} — {$fileName}
"; + + // Smaller image preview for better performance + $html = "
" . + "{$fileName}" . + "
" . + "{$name}" . + "{$fileName}" . + "
"; return [$url => $html]; })->toArray(); }) ->searchable() + ->preload() ->required(), ]) ->action(function (array $data, RichEditor $component) { From 93c977d1f5243579b58ba5dd46fb964e138e0f96 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 3 Jan 2026 17:26:18 +0000 Subject: [PATCH 10/10] added fixes: warning some files likely wont be needed and wer added by ai to fix things that were no longer needed !!! --- .../Resources/Entries/Schemas/EntryForm.php | 104 +++++++++++------ app/Livewire/GalleryPicker.php | 108 ++++++++++++++++++ resources/js/app.js | 10 ++ .../featured-image-display.blade.php | 32 ++++++ .../views/livewire/gallery-picker.blade.php | 72 ++++++++++++ 5 files changed, 288 insertions(+), 38 deletions(-) create mode 100644 app/Livewire/GalleryPicker.php create mode 100644 resources/views/components/featured-image-display.blade.php create mode 100644 resources/views/livewire/gallery-picker.blade.php diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index 02b5aee..6c029f6 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -40,6 +40,7 @@ class EntryForm ->disk('public') ->visibility('public') ->columnSpanFull() + ->dehydrated(false) ->hintAction( Action::make('featured_picker') ->label('Pick from Gallery') @@ -49,40 +50,36 @@ class EntryForm ->label('Select an existing image') ->allowHtml() ->options(function () { - return Media::latest() - ->limit(30) // Limit to 30 most recent items for performance - ->get(['id', 'file_name', 'name', 'uuid', 'collection_name', 'model_type', 'model_id', 'disk']) - ->filter(function (Media $item) { - // Only include media items that have a valid disk - return $item->disk !== null; - }) + return Media::where('model_type', 'temp') + ->where('model_id', 0) + ->where('disk', 'public') + ->latest() + ->limit(30) + ->get(['id', 'file_name', 'name', 'disk']) ->mapWithKeys(function (Media $item) { try { $url = $item->getUrl(); + $fileName = e($item->file_name); + $name = e($item->name ?? ''); + + $html = "
" . + "{$fileName}" . + "
" . + "{$name}" . + "{$fileName}" . + "
"; + + return [$item->id => $html]; } catch (\Exception $e) { - // Skip items that can't generate URLs return []; } - - $fileName = e($item->file_name); - $name = e($item->name ?? ''); - - // Smaller image preview for better performance - $html = "
" . - "{$fileName}" . - "
" . - "{$name}" . - "{$fileName}" . - "
"; - - return [$item->id => $html]; })->toArray(); }) ->searchable() ->preload() ->required(), ]) - ->action(function (array $data, SpatieMediaLibraryFileUpload $component) { + ->action(function (array $data, SpatieMediaLibraryFileUpload $component): void { $record = $component->getRecord(); if (!$record) { @@ -92,25 +89,56 @@ class EntryForm ->send(); return; } - - if ($mediaItem = Media::find($data['image_id'])) { - // Clear existing featured image - $record->clearMediaCollection('featured-image'); - - // Associate the existing media with this entry instead of copying - $mediaItem->update([ - 'model_type' => get_class($record), - 'model_id' => $record->id, - 'collection_name' => 'featured-image' - ]); - - // Update component state - $component->state([$mediaItem->uuid]); - + + if (!$data['image_id']) { + return; + } + + $sourceMedia = Media::find($data['image_id']); + if (!$sourceMedia || !file_exists($sourceMedia->getPath())) { + \Filament\Notifications\Notification::make() + ->danger() + ->title('Image file not found') + ->send(); + return; + } + + $sourceFile = $sourceMedia->getPath(); + $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; + copy($sourceFile, $tempCopy); + + try { + // Verify record has ID + if (!$record->id) { + \Filament\Notifications\Notification::make() + ->danger() + ->title('Entry must be saved first') + ->send(); + return; + } + + // Add the copy to the entry's featured-image collection + $newMedia = $record->addMedia($tempCopy) + ->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME)) + ->usingFileName($sourceMedia->file_name) + ->toMediaCollection('featured-image', 'public'); + + // Dispatch event for app.js to handle + $component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]); + \Filament\Notifications\Notification::make() ->success() - ->title('Featured image set') + ->title('Image added to featured image') ->send(); + } catch (\Exception $e) { + \Filament\Notifications\Notification::make() + ->danger() + ->title('Error: ' . $e->getMessage()) + ->send(); + } finally { + if (file_exists($tempCopy)) { + unlink($tempCopy); + } } }) ), diff --git a/app/Livewire/GalleryPicker.php b/app/Livewire/GalleryPicker.php new file mode 100644 index 0000000..c1da485 --- /dev/null +++ b/app/Livewire/GalleryPicker.php @@ -0,0 +1,108 @@ +entryId = $entryId; + $this->loadMediaItems(); + $this->showModal = true; + } + + public function loadMediaItems(): void + { + $this->mediaItems = Media::where('model_type', 'temp') + ->where('model_id', 0) + ->where('disk', 'public') + ->latest() + ->limit(30) + ->get(['id', 'file_name', 'name', 'disk']) + ->toArray(); + } + + public function selectMedia($mediaId): void + { + $this->selectedMediaId = $mediaId; + } + + public function copyToEntry(): void + { + if (!$this->selectedMediaId || !$this->entryId) { + $this->dispatch('notify-error', ['message' => 'Please select an image']); + return; + } + + $sourceMedia = Media::find($this->selectedMediaId); + if (!$sourceMedia) { + $this->dispatch('notify-error', ['message' => 'Media not found']); + return; + } + + try { + // Get the entry + $entry = \App\Models\Entry::find($this->entryId); + if (!$entry) { + $this->dispatch('notify-error', ['message' => 'Entry not found']); + return; + } + + // Get source file + $sourceFile = $sourceMedia->getPath(); + if (!file_exists($sourceFile)) { + $this->dispatch('notify-error', ['message' => 'Source file not found']); + return; + } + + // Create temp copy + $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; + copy($sourceFile, $tempCopy); + + try { + // Clear existing featured image + $entry->clearMediaCollection('featured-image'); + + // Add to entry + $newMedia = $entry->addMedia($tempCopy) + ->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME)) + ->usingFileName($sourceMedia->file_name) + ->toMediaCollection('featured-image', 'public'); + + // Close modal and notify + $this->showModal = false; + $this->selectedMediaId = null; + $this->dispatch('media-selected', ['mediaId' => $newMedia->id, 'fileName' => $newMedia->file_name]); + $this->dispatch('notify-success', ['message' => 'Image added to entry']); + } finally { + if (file_exists($tempCopy)) { + unlink($tempCopy); + } + } + } catch (\Exception $e) { + $this->dispatch('notify-error', ['message' => 'Error: ' . $e->getMessage()]); + } + } + + public function closePicker(): void + { + $this->showModal = false; + $this->selectedMediaId = null; + } + + public function render() + { + return view('livewire.gallery-picker'); + } +} diff --git a/resources/js/app.js b/resources/js/app.js index b52c9ea..22e1c9c 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -33,6 +33,16 @@ document.addEventListener('livewire:init', () => { } } }); + + Livewire.on('featured-image-added', (data) => { + console.log('Received featured-image-added event:', data); + const payload = Array.isArray(data) ? data[0] : data; + + // Reload the page to show the updated featured image + setTimeout(() => { + window.location.reload(); + }, 500); + }); }); console.log('Testing if app.js is still running'); \ No newline at end of file diff --git a/resources/views/components/featured-image-display.blade.php b/resources/views/components/featured-image-display.blade.php new file mode 100644 index 0000000..437552f --- /dev/null +++ b/resources/views/components/featured-image-display.blade.php @@ -0,0 +1,32 @@ +
+ @php + $record = $this->data; + if ($record && isset($record['id'])) { + $entry = \App\Models\Entry::find($record['id']); + $media = $entry ? $entry->getMedia('featured-image') : []; + } else { + $media = []; + } + @endphp + +
+ + @if(count($media) > 0) +
+ @foreach($media as $item) +
+
+ +
+

{{ $item->name }}

+

{{ $item->file_name }}

+
+
+
+ @endforeach +
+ @else +

No featured image selected

+ @endif +
+
diff --git a/resources/views/livewire/gallery-picker.blade.php b/resources/views/livewire/gallery-picker.blade.php new file mode 100644 index 0000000..103b66b --- /dev/null +++ b/resources/views/livewire/gallery-picker.blade.php @@ -0,0 +1,72 @@ +
+ @if($showModal ?? false) +
+
+ +
+ + +
+
+
+

+ Select Image from Gallery +

+ +
+ + @if(empty($mediaItems ?? [])) +

No images in gallery

+ @else +
+ @foreach($mediaItems ?? [] as $item) + + @endforeach +
+ @endif +
+ +
+ + +
+
+
+
+ @endif +