components([ Select::make('type') ->options([ 'article' => 'Article', 'card' => 'Card', 'text' => 'Text', 'image' => 'Image', ]) ->default('article') ->required() ->live(), TextInput::make('title') ->required() ->live(onBlur: true) ->afterStateUpdated(function ($state, $set): void { $set('slug', Str::slug((string) $state)); }), TextInput::make('slug') ->required() ->visible(fn($get) => $get('type') === 'article') ->dehydrated() ->readOnly(), Textarea::make('description') ->visible(fn($get) => $get('type') === 'article') ->columnSpanFull(), SpatieTagsInput::make('tags') ->type('entry-tags') ->visible(fn($get) => $get('type') === 'article') ->columnSpanFull(), SpatieMediaLibraryFileUpload::make('featured_image') ->visible( fn($get) => $get('type') === 'article' || $get('type') === 'image' ) ->collection('featured-image') ->image() ->imageEditor() ->disk(config('media-library.disk_name', 'public')) ->visibility('public') ->columnSpanFull() ->dehydrated(false) ->saveUploadedFileUsing(function ($file, $record) { $diskName = config('media-library.disk_name', 'public'); if (config('app.env') === 'local') { Log::info('Featured Image Upload Debug', [ 'disk' => $diskName, 'file_name' => $file->getClientOriginalName(), 'file_size' => $file->getSize(), 'file_mime' => $file->getMimeType(), 'file_path' => $file->getRealPath(), 'record_id' => $record?->id, 'aws_config' => [ 'bucket' => config('filesystems.disks.s3.bucket'), 'region' => config('filesystems.disks.s3.region'), 'key_exists' => !empty(config('filesystems.disks.s3.key')), 'secret_exists' => !empty(config('filesystems.disks.s3.secret')), ] ]); } try { if (!$record) { throw new \Exception('Record not found during upload'); } // Test S3 connection if using S3 if ($diskName === 's3') { $disk = \Storage::disk('s3'); // Test basic S3 connectivity $testFile = 'test-' . time() . '.txt'; $disk->put($testFile, 'test content'); $disk->delete($testFile); if (config('app.env') === 'local') { Log::info('S3 connectivity test passed'); } } // Use addMedia with the file directly, not addMediaFromRequest // Generate secure filename similar to Livewire temp files $originalName = $file->getClientOriginalName(); $extension = pathinfo($originalName, PATHINFO_EXTENSION); $baseName = pathinfo($originalName, PATHINFO_FILENAME); // Generate secure filename with encoded original name $encodedName = base64_encode($originalName); $secureFileName = Str::random(32) . '-meta' . $encodedName . '-.' . $extension; $media = $record->addMedia($file->getRealPath()) ->usingName($baseName) // Keep original name for display/search ->usingFileName($secureFileName) // Use secure filename for storage ->toMediaCollection('featured-image', $diskName); if (config('app.env') === 'local') { Log::info('Featured Image Upload Success', [ 'media_id' => $media->id, 'media_url' => $media->getUrl(), 'media_path' => $media->getPathRelativeToRoot(), 'disk' => $media->disk ]); } return $media->getUrl(); } catch (\Exception $e) { Log::error('Featured Image Upload Failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'disk' => $diskName, 'file_name' => $file->getClientOriginalName() ]); // Also show error to user \Filament\Notifications\Notification::make() ->danger() ->title('Upload Failed') ->body($e->getMessage()) ->persistent() ->send(); throw $e; } }) ->hintAction( Action::make('featured_picker') ->label('Featured Image from Gallery') ->icon('heroicon-m-photo') ->extraAttributes(['id' => 'featured-picker-button']) ->schema([ Select::make('image_id') ->label('Select an existing image') ->allowHtml() ->options(function () { $currentDisk = config('media-library.disk_name', 'public'); return Media::where('disk', $currentDisk) ->latest() ->limit(50) ->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) { return []; } })->toArray(); }) ->searchable() ->preload() ->required(), ]) ->action(function (array $data, SpatieMediaLibraryFileUpload $component): void { $record = $component->getRecord(); $diskName = config('media-library.disk_name', 'public'); if (config('app.env') === 'local') { Log::info('Featured Image Picker Action Debug', [ 'disk' => $diskName, 'record_id' => $record?->id, 'image_id' => $data['image_id'] ?? null ]); } if (! $record) { \Filament\Notifications\Notification::make() ->warning() ->title('Save the entry first') ->send(); return; } if (! $data['image_id']) { return; } $sourceMedia = Media::find($data['image_id']); if (! $sourceMedia) { Log::error('Source media not found', ['image_id' => $data['image_id']]); \Filament\Notifications\Notification::make() ->danger() ->title('Source image not found in database') ->send(); return; } try { // For S3, we need to handle file copying differently if ($sourceMedia->disk === 's3') { if (config('app.env') === 'local') { Log::info('Copying S3 media to new collection', [ 'source_disk' => $sourceMedia->disk, 'source_path' => $sourceMedia->getPathRelativeToRoot(), 'target_disk' => $diskName ]); } // Copy from S3 to S3 or download temporarily $sourceDisk = \Storage::disk($sourceMedia->disk); $sourceContent = $sourceDisk->get($sourceMedia->getPathRelativeToRoot()); if (!$sourceContent) { throw new \Exception('Could not read source file from S3'); } $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; file_put_contents($tempCopy, $sourceContent); } else { // Local file handling $sourceFile = $sourceMedia->getPath(); if (! file_exists($sourceFile)) { throw new \Exception('Source file not found on disk'); } $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; copy($sourceFile, $tempCopy); } // 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', $diskName); if (config('app.env') === 'local') { Log::info('Featured Image Picker Success', [ 'new_media_id' => $newMedia->id, 'new_media_disk' => $newMedia->disk, 'new_media_url' => $newMedia->getUrl() ]); } // Dispatch event for app.js to handle $component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]); \Filament\Notifications\Notification::make() ->success() ->title('Image added to featured image') ->send(); } catch (\Exception $e) { Log::error('Featured Image Picker Failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'source_media_id' => $data['image_id'], 'disk' => $diskName ]); \Filament\Notifications\Notification::make() ->danger() ->title('Error: ' . $e->getMessage()) ->persistent() ->send(); } finally { if (isset($tempCopy) && file_exists($tempCopy)) { unlink($tempCopy); } } }) ), Toggle::make('is_published') ->required(), Toggle::make('is_featured') ->required(), TextInput::make('priority') ->label('Priority') ->numeric() ->default(0) ->required(), DatePicker::make('published_at') ->visible(fn($get) => $get('type') === 'article'), Select::make('category_id') ->label('Category') ->options(function () { return Category::all() ->pluck('name', 'id') ->toArray(); }) ->searchable(), TextInput::make('call_to_action_text') ->label('Call to Action Text') ->visible(fn($get) => $get('type') !== 'article'), TextInput::make('call_to_action_link') ->label('Call to Action URL') ->visible(fn($get) => $get('type') !== 'article'), RichEditor::make('content') ->visible(fn($get) => $get('type') !== 'image') ->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 () { 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; }) ->mapWithKeys(function (Media $item) { try { $url = $item->getUrl(); } 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 [$url => $html]; })->toArray(); }) ->searchable() ->preload() ->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' => "", ]); }) ), ]); } }