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 = "
".
+ "

".
+ "
{$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