Compare commits
10 commits
5ea0ddce23
...
93c977d1f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c977d1f5 | ||
|
|
8e1650653b | ||
|
|
6d1d88542e | ||
|
|
a94a34ce3b | ||
|
|
c49249ee20 | ||
|
|
340b466ade | ||
|
|
9f01d44c9d | ||
|
|
d24b9b0732 | ||
|
|
02884d4e2b | ||
|
|
d40b87438d |
18 changed files with 971 additions and 5 deletions
112
app/Console/Commands/MoveMediaToPublic.php
Normal file
112
app/Console/Commands/MoveMediaToPublic.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,18 @@
|
||||||
|
|
||||||
namespace App\Filament\Resources\Entries\Schemas;
|
namespace App\Filament\Resources\Entries\Schemas;
|
||||||
|
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\RichEditor;
|
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\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
|
|
||||||
class EntryForm
|
class EntryForm
|
||||||
{
|
{
|
||||||
|
|
@ -18,22 +23,183 @@ class EntryForm
|
||||||
->components([
|
->components([
|
||||||
TextInput::make('title')
|
TextInput::make('title')
|
||||||
->required()
|
->required()
|
||||||
->reactive()
|
->live(onBlur: true)
|
||||||
->afterStateUpdated(function ($state, $set): void {
|
->afterStateUpdated(function ($state, $set): void {
|
||||||
$set('slug', Str::slug((string) $state));
|
$set('slug', Str::slug((string) $state));
|
||||||
}),
|
}),
|
||||||
TextInput::make('slug')
|
TextInput::make('slug')
|
||||||
->required()
|
->required()
|
||||||
->disabled(),
|
->dehydrated()
|
||||||
|
->readOnly(),
|
||||||
Textarea::make('description')
|
Textarea::make('description')
|
||||||
->columnSpanFull(),
|
->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')
|
Toggle::make('is_published')
|
||||||
->required(),
|
->required(),
|
||||||
Toggle::make('is_featured')
|
Toggle::make('is_featured')
|
||||||
->required(),
|
->required(),
|
||||||
DatePicker::make('published_at'),
|
DatePicker::make('published_at'),
|
||||||
RichEditor::make('content')
|
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=''>",
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Filament\Resources\Entries\Schemas;
|
namespace App\Filament\Resources\Entries\Schemas;
|
||||||
|
|
||||||
use Filament\Infolists\Components\IconEntry;
|
use Filament\Infolists\Components\IconEntry;
|
||||||
|
use Filament\Infolists\Components\SpatieMediaLibraryImageEntry;
|
||||||
use Filament\Infolists\Components\TextEntry;
|
use Filament\Infolists\Components\TextEntry;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
|
@ -17,6 +18,9 @@ class EntryInfolist
|
||||||
TextEntry::make('description')
|
TextEntry::make('description')
|
||||||
->placeholder('-')
|
->placeholder('-')
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
SpatieMediaLibraryImageEntry::make('featured_image')
|
||||||
|
->collection('featured-image')
|
||||||
|
->columnSpanFull(),
|
||||||
IconEntry::make('is_published')
|
IconEntry::make('is_published')
|
||||||
->boolean(),
|
->boolean(),
|
||||||
IconEntry::make('is_featured')
|
IconEntry::make('is_featured')
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use Filament\Actions\DeleteBulkAction;
|
||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Actions\ViewAction;
|
use Filament\Actions\ViewAction;
|
||||||
use Filament\Tables\Columns\IconColumn;
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
|
@ -16,6 +17,11 @@ class EntriesTable
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
|
SpatieMediaLibraryImageColumn::make('featured_image')
|
||||||
|
->collection('featured-image')
|
||||||
|
->circular()
|
||||||
|
->stacked()
|
||||||
|
->limit(3),
|
||||||
TextColumn::make('title')
|
TextColumn::make('title')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
TextColumn::make('slug')
|
TextColumn::make('slug')
|
||||||
|
|
|
||||||
58
app/Filament/Resources/Media/MediaResource.php
Normal file
58
app/Filament/Resources/Media/MediaResource.php
Normal 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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Filament/Resources/Media/Pages/CreateMedia.php
Normal file
61
app/Filament/Resources/Media/Pages/CreateMedia.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/Filament/Resources/Media/Pages/EditMedia.php
Normal file
72
app/Filament/Resources/Media/Pages/EditMedia.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Media/Pages/ListMedia.php
Normal file
19
app/Filament/Resources/Media/Pages/ListMedia.php
Normal 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Media/Pages/ViewMedia.php
Normal file
19
app/Filament/Resources/Media/Pages/ViewMedia.php
Normal 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Filament/Resources/Media/Schemas/MediaForm.php
Normal file
59
app/Filament/Resources/Media/Schemas/MediaForm.php
Normal 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);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Filament/Resources/Media/Schemas/MediaInfolist.php
Normal file
36
app/Filament/Resources/Media/Schemas/MediaInfolist.php
Normal 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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
app/Filament/Resources/Media/Tables/MediaTable.php
Normal file
86
app/Filament/Resources/Media/Tables/MediaTable.php
Normal 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();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
app/Livewire/GalleryPicker.php
Normal file
108
app/Livewire/GalleryPicker.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ use Filament\Pages\Dashboard;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Filament\PanelProvider;
|
use Filament\PanelProvider;
|
||||||
use Filament\Support\Colors\Color;
|
use Filament\Support\Colors\Color;
|
||||||
|
use Filament\Support\Facades\FilamentView;
|
||||||
|
use Filament\View\PanelsRenderHook;
|
||||||
use Filament\Widgets\AccountWidget;
|
use Filament\Widgets\AccountWidget;
|
||||||
use Filament\Widgets\FilamentInfoWidget;
|
use Filament\Widgets\FilamentInfoWidget;
|
||||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
|
|
@ -56,4 +58,12 @@ class AdminPanelProvider extends PanelProvider
|
||||||
Authenticate::class,
|
Authenticate::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
FilamentView::registerRenderHook(
|
||||||
|
PanelsRenderHook::BODY_END,
|
||||||
|
fn (): string => \Illuminate\Support\Facades\Blade::render('@vite("resources/js/app.js")'),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"filament/filament": "^4.0",
|
"filament/filament": "^4.0",
|
||||||
"filament/spatie-laravel-media-library-plugin": "^4.0",
|
|
||||||
"laravel/fortify": "^1.30",
|
"laravel/fortify": "^1.30",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
|
|
|
||||||
|
|
@ -1 +1,48 @@
|
||||||
console.log('App.js is loaded');
|
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');
|
||||||
32
resources/views/components/featured-image-display.blade.php
Normal file
32
resources/views/components/featured-image-display.blade.php
Normal 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>
|
||||||
72
resources/views/livewire/gallery-picker.blade.php
Normal file
72
resources/views/livewire/gallery-picker.blade.php
Normal 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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue