Compare commits

..

10 commits

Author SHA1 Message Date
jon brookes
93c977d1f5 added fixes: warning
some files likely wont be needed and wer added by ai to fix things that were no longer needed !!!
2026-01-03 17:26:18 +00:00
jon brookes
8e1650653b fix: partical images broken
public seems to be holding

still issues with picking images - not saved
2026-01-03 15:12:34 +00:00
jon brookes
6d1d88542e fix: storage to public
private was breaking things bad
2026-01-03 14:52:52 +00:00
jon brookes
a94a34ce3b fix: storage to public
this was breaking things bad
2026-01-03 14:52:04 +00:00
jon brookes
c49249ee20 fix: partical fix for featured
imaes are added, seem semi permanent

disapapar in media lib
2026-01-03 14:25:23 +00:00
jon brookes
340b466ade feat: enhance featured image upload
with gallery selection and preview
2026-01-03 13:35:38 +00:00
jon brookes
9f01d44c9d 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
2026-01-03 13:22:14 +00:00
jon brookes
d24b9b0732 removed: filament/spatie-laravel-media-library-plugin 2026-01-03 12:59:56 +00:00
jon brookes
02884d4e2b feat: implement Spatie Media Library integration
with CRUD operations and media management UI
2026-01-02 18:59:24 +00:00
jon brookes
d40b87438d fix: add render hook for Vite in AdminPanelProvider 2026-01-02 16:57:30 +00:00
18 changed files with 971 additions and 5 deletions

View file

@ -0,0 +1,112 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class MoveMediaToPublic extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'media:move-to-public {--dry-run : Show what would be moved without actually moving files}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Move all media files from private/local disk to public disk';
/**
* Execute the console command.
*/
public function handle(): int
{
$dryRun = $this->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;
}
}

View file

@ -2,13 +2,18 @@
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\SpatieMediaLibraryFileUpload;
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;
class EntryForm
{
@ -18,22 +23,183 @@ 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(),
SpatieMediaLibraryFileUpload::make('featured_image')
->collection('featured-image')
->image()
->imageEditor()
->disk('public')
->visibility('public')
->columnSpanFull()
->dehydrated(false)
->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::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 = "<div class='flex items-center gap-3'>" .
"<img src='{$url}' class='rounded' style='width:60px;height:60px;object-fit:cover;' alt='{$fileName}' loading='lazy' />" .
"<div class='flex flex-col'>" .
"<span class='font-medium text-sm'>{$name}</span>" .
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
"</div></div>";
return [$item->id => $html];
} catch (\Exception $e) {
return [];
}
})->toArray();
})
->searchable()
->preload()
->required(),
])
->action(function (array $data, SpatieMediaLibraryFileUpload $component): void {
$record = $component->getRecord();
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 || !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('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);
}
}
})
),
Toggle::make('is_published')
->required(),
Toggle::make('is_featured')
->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 () {
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 = "<div class='flex items-center gap-3'>" .
"<img src='{$url}' class='rounded' style='width:60px;height:60px;object-fit:cover;' alt='{$fileName}' loading='lazy' />" .
"<div class='flex flex-col'>" .
"<span class='font-medium text-sm'>{$name}</span>" .
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
"</div></div>";
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' => "<img src='{$data['image_url']}' alt=''>",
]);
})
),
]);
}
}

View file

@ -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')

View file

@ -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-image')
->circular()
->stacked()
->limit(3),
TextColumn::make('title')
->searchable(),
TextColumn::make('slug')

View file

@ -0,0 +1,58 @@
<?php
namespace App\Filament\Resources\Media;
use App\Filament\Resources\Media\Pages\CreateMedia;
use App\Filament\Resources\Media\Pages\EditMedia;
use App\Filament\Resources\Media\Pages\ListMedia;
use App\Filament\Resources\Media\Pages\ViewMedia;
use App\Filament\Resources\Media\Schemas\MediaForm;
use App\Filament\Resources\Media\Schemas\MediaInfolist;
use App\Filament\Resources\Media\Tables\MediaTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class MediaResource extends Resource
{
protected static ?string $model = Media::class;
protected static ?string $recordTitleAttribute = 'file_name';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return MediaForm::configure($schema);
}
public static function infolist(Schema $schema): Schema
{
return MediaInfolist::configure($schema);
}
public static function table(Table $table): Table
{
return MediaTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListMedia::route('/'),
'create' => CreateMedia::route('/create'),
'view' => ViewMedia::route('/{record}'),
'edit' => EditMedia::route('/{record}/edit'),
];
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Storage;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class CreateMedia extends CreateRecord
{
protected static string $resource = MediaResource::class;
protected ?string $uploadedFile = null;
protected function mutateFormDataBeforeCreate(array $data): array
{
// Extract the file data before creating the media record
$file = $data['file'] ?? null;
unset($data['file']);
// Store file path for later
$this->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);
}
}
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Storage;
class EditMedia extends EditRecord
{
protected static string $resource = MediaResource::class;
protected ?string $uploadedFile = null;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
// Extract the file data if a new file was uploaded
$file = $data['file'] ?? null;
unset($data['file']);
// Only update file-related fields if a new file was uploaded
if ($file && $file !== $this->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);
}
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListMedia extends ListRecords
{
protected static string $resource = MediaResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Media\Pages;
use App\Filament\Resources\Media\MediaResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewMedia extends ViewRecord
{
protected static string $resource = MediaResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make(),
];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Filament\Resources\Media\Schemas;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
class MediaForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->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);
}),
]);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Filament\Resources\Media\Schemas;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
class MediaInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->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(),
]);
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace App\Filament\Resources\Media\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class MediaTable
{
public static function configure(Table $table): Table
{
return $table
->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();
});
}),
]),
]);
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class GalleryPicker extends Component
{
#[\Livewire\Attributes\Reactive]
public $entryId;
public $mediaItems = [];
public $selectedMediaId = null;
public $showModal = false;
#[\Livewire\Attributes\On('open-gallery-picker')]
public function openPicker($entryId): void
{
$this->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');
}
}

View file

@ -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")'),
);
}
}

View file

@ -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",

View file

@ -1 +1,48 @@
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 <img> 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);
}
}
});
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');

View file

@ -0,0 +1,32 @@
<div>
@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
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Featured Image</label>
@if(count($media) > 0)
<div class="space-y-2">
@foreach($media as $item)
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
<div class="flex items-center gap-3">
<img src="{{ $item->getUrl() }}" alt="" class="w-16 h-16 object-cover rounded">
<div>
<p class="text-sm font-medium">{{ $item->name }}</p>
<p class="text-xs text-gray-500">{{ $item->file_name }}</p>
</div>
</div>
</div>
@endforeach
</div>
@else
<p class="text-sm text-gray-500">No featured image selected</p>
@endif
</div>
</div>

View file

@ -0,0 +1,72 @@
<div>
@if($showModal ?? false)
<div class="fixed inset-0 z-50 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- Background overlay -->
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" wire:click="closePicker()"></div>
<!-- Modal panel -->
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">
Select Image from Gallery
</h3>
<button wire:click="closePicker()" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
@if(empty($mediaItems ?? []))
<p class="text-center text-gray-500 py-8">No images in gallery</p>
@else
<div class="grid grid-cols-3 gap-4 max-h-96 overflow-y-auto">
@foreach($mediaItems ?? [] as $item)
<button
wire:click="selectMedia({{ $item['id'] }})"
class="relative group overflow-hidden rounded-lg border-2 transition-colors {{ ($selectedMediaId ?? null) === $item['id'] ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300' }}"
>
<img
src="{{ asset('storage/' . $item['disk'] . '/' . $item['id'] . '/' . $item['file_name']) }}"
alt="{{ $item['name'] ?? $item['file_name'] }}"
class="w-full h-32 object-cover"
loading="lazy"
/>
@if(($selectedMediaId ?? null) === $item['id'])
<div class="absolute inset-0 bg-blue-500 bg-opacity-20 flex items-center justify-center">
<svg class="h-8 w-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</div>
@endif
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity truncate">
{{ $item['name'] ?? $item['file_name'] }}
</div>
</button>
@endforeach
</div>
@endif
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-3">
<button
wire:click="copyToEntry"
:disabled="!($selectedMediaId ?? null)"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed sm:w-auto sm:text-sm"
>
Add Image
</button>
<button
wire:click="closePicker()"
class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</div>
</div>
</div>
@endif
</div>