Compare commits
13 commits
dev
...
feat/spati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c977d1f5 | ||
|
|
8e1650653b | ||
|
|
6d1d88542e | ||
|
|
a94a34ce3b | ||
|
|
c49249ee20 | ||
|
|
340b466ade | ||
|
|
9f01d44c9d | ||
|
|
d24b9b0732 | ||
|
|
02884d4e2b | ||
|
|
d40b87438d | ||
|
|
5ea0ddce23 | ||
|
|
6cf8d5dfd4 | ||
|
|
a0a1c08ece |
38 changed files with 2326 additions and 15 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -21,3 +21,4 @@ yarn-error.log
|
||||||
/.nova
|
/.nova
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
|
.vite
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,9 @@
|
||||||
|
|
||||||
added: laravel 12
|
added: laravel 12
|
||||||
|
|
||||||
added: AGPLv3
|
added: AGPLv3
|
||||||
|
|
||||||
|
## 2026-01-02
|
||||||
|
|
||||||
|
added initial model and filament resource
|
||||||
|
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -219,7 +219,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
test
|
share-lt
|
||||||
Copyright (C) 2026 jon
|
Copyright (C) 2026 jon
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/Filament/Resources/Entries/EntryResource.php
Normal file
58
app/Filament/Resources/Entries/EntryResource.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Entries;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Entries\Pages\CreateEntry;
|
||||||
|
use App\Filament\Resources\Entries\Pages\EditEntry;
|
||||||
|
use App\Filament\Resources\Entries\Pages\ListEntries;
|
||||||
|
use App\Filament\Resources\Entries\Pages\ViewEntry;
|
||||||
|
use App\Filament\Resources\Entries\Schemas\EntryForm;
|
||||||
|
use App\Filament\Resources\Entries\Schemas\EntryInfolist;
|
||||||
|
use App\Filament\Resources\Entries\Tables\EntriesTable;
|
||||||
|
use App\Models\Entry;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class EntryResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Entry::class;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'title';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return EntryForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function infolist(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return EntryInfolist::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return EntriesTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListEntries::route('/'),
|
||||||
|
'create' => CreateEntry::route('/create'),
|
||||||
|
'view' => ViewEntry::route('/{record}'),
|
||||||
|
'edit' => EditEntry::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Entries/Pages/CreateEntry.php
Normal file
11
app/Filament/Resources/Entries/Pages/CreateEntry.php
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Entries\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Entries\EntryResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateEntry extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = EntryResource::class;
|
||||||
|
}
|
||||||
21
app/Filament/Resources/Entries/Pages/EditEntry.php
Normal file
21
app/Filament/Resources/Entries/Pages/EditEntry.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Entries\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Entries\EntryResource;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditEntry extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = EntryResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ViewAction::make(),
|
||||||
|
DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Entries/Pages/ListEntries.php
Normal file
19
app/Filament/Resources/Entries/Pages/ListEntries.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Entries\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Entries\EntryResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListEntries extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = EntryResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Entries/Pages/ViewEntry.php
Normal file
19
app/Filament/Resources/Entries/Pages/ViewEntry.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Entries\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Entries\EntryResource;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
|
|
||||||
|
class ViewEntry extends ViewRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = EntryResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
EditAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
205
app/Filament/Resources/Entries/Schemas/EntryForm.php
Normal file
205
app/Filament/Resources/Entries/Schemas/EntryForm.php
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Entries\Schemas;
|
||||||
|
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\RichEditor;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
|
|
||||||
|
class EntryForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('title')
|
||||||
|
->required()
|
||||||
|
->live(onBlur: true)
|
||||||
|
->afterStateUpdated(function ($state, $set): void {
|
||||||
|
$set('slug', Str::slug((string) $state));
|
||||||
|
}),
|
||||||
|
TextInput::make('slug')
|
||||||
|
->required()
|
||||||
|
->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()
|
||||||
|
->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=''>",
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Filament/Resources/Entries/Schemas/EntryInfolist.php
Normal file
42
app/Filament/Resources/Entries/Schemas/EntryInfolist.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class EntryInfolist
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextEntry::make('title'),
|
||||||
|
TextEntry::make('slug'),
|
||||||
|
TextEntry::make('description')
|
||||||
|
->placeholder('-')
|
||||||
|
->columnSpanFull(),
|
||||||
|
SpatieMediaLibraryImageEntry::make('featured_image')
|
||||||
|
->collection('featured-image')
|
||||||
|
->columnSpanFull(),
|
||||||
|
IconEntry::make('is_published')
|
||||||
|
->boolean(),
|
||||||
|
IconEntry::make('is_featured')
|
||||||
|
->boolean(),
|
||||||
|
TextEntry::make('published_at')
|
||||||
|
->date()
|
||||||
|
->placeholder('-'),
|
||||||
|
TextEntry::make('content')
|
||||||
|
->placeholder('-')
|
||||||
|
->columnSpanFull(),
|
||||||
|
TextEntry::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->placeholder('-'),
|
||||||
|
TextEntry::make('updated_at')
|
||||||
|
->dateTime()
|
||||||
|
->placeholder('-'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/Filament/Resources/Entries/Tables/EntriesTable.php
Normal file
58
app/Filament/Resources/Entries/Tables/EntriesTable.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Entries\Tables;
|
||||||
|
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
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;
|
||||||
|
|
||||||
|
class EntriesTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
SpatieMediaLibraryImageColumn::make('featured_image')
|
||||||
|
->collection('featured-image')
|
||||||
|
->circular()
|
||||||
|
->stacked()
|
||||||
|
->limit(3),
|
||||||
|
TextColumn::make('title')
|
||||||
|
->searchable(),
|
||||||
|
TextColumn::make('slug')
|
||||||
|
->searchable(),
|
||||||
|
IconColumn::make('is_published')
|
||||||
|
->boolean(),
|
||||||
|
IconColumn::make('is_featured')
|
||||||
|
->boolean(),
|
||||||
|
TextColumn::make('published_at')
|
||||||
|
->date()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
TextColumn::make('updated_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
ViewAction::make(),
|
||||||
|
EditAction::make(),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Models/Entry.php
Normal file
42
app/Models/Entry.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider;
|
||||||
|
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
|
||||||
|
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
|
class Entry extends Model implements HasRichContent, HasMedia
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
use InteractsWithMedia, InteractsWithRichContent;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'is_published',
|
||||||
|
'is_featured',
|
||||||
|
'published_at',
|
||||||
|
'content',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up rich content configuration for media library integration
|
||||||
|
*/
|
||||||
|
public function setUpRichContent(): void
|
||||||
|
{
|
||||||
|
$this->registerRichContent('content')
|
||||||
|
->fileAttachmentProvider(
|
||||||
|
SpatieMediaLibraryFileAttachmentProvider::make()
|
||||||
|
->collection('content-attachments')
|
||||||
|
->preserveFilenames()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -29,7 +31,7 @@ class AdminPanelProvider extends PanelProvider
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->login()
|
->login()
|
||||||
->colors([
|
->colors([
|
||||||
'primary' => Color::Amber,
|
'primary' => Color::Blue,
|
||||||
])
|
])
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
||||||
|
|
@ -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")'),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
"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",
|
||||||
"livewire/flux": "^2.9.0"
|
"livewire/flux": "^2.9.0",
|
||||||
|
"spatie/laravel-medialibrary": "^11.17"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|
|
||||||
507
composer.lock
generated
507
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "ed91eaf8381afba35eea6bfdd94e4e18",
|
"content-hash": "bec347dcc3a450fc682920a766cbe019",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
|
|
@ -565,6 +565,83 @@
|
||||||
],
|
],
|
||||||
"time": "2024-07-16T11:13:48+00:00"
|
"time": "2024-07-16T11:13:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "composer/semver",
|
||||||
|
"version": "3.4.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/composer/semver.git",
|
||||||
|
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||||
|
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^5.3.2 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.11",
|
||||||
|
"symfony/phpunit-bridge": "^3 || ^7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Semver\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nils Adermann",
|
||||||
|
"email": "naderman@naderman.de",
|
||||||
|
"homepage": "http://www.naderman.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rob Bast",
|
||||||
|
"email": "rob.bast@gmail.com",
|
||||||
|
"homepage": "http://robbast.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Semver library that offers utilities, version constraint parsing and validation.",
|
||||||
|
"keywords": [
|
||||||
|
"semantic",
|
||||||
|
"semver",
|
||||||
|
"validation",
|
||||||
|
"versioning"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||||
|
"issues": "https://github.com/composer/semver/issues",
|
||||||
|
"source": "https://github.com/composer/semver/tree/3.4.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://packagist.com",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/composer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-20T19:15:30+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "danharrin/date-format-converter",
|
"name": "danharrin/date-format-converter",
|
||||||
"version": "v0.3.1",
|
"version": "v0.3.1",
|
||||||
|
|
@ -1432,6 +1509,43 @@
|
||||||
},
|
},
|
||||||
"time": "2025-12-30T13:02:44+00:00"
|
"time": "2025-12-30T13:02:44+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "filament/spatie-laravel-media-library-plugin",
|
||||||
|
"version": "v4.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/filamentphp/spatie-laravel-media-library-plugin.git",
|
||||||
|
"reference": "73748df28a9c2e8c34d2c02f9314c330602e1830"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/filamentphp/spatie-laravel-media-library-plugin/zipball/73748df28a9c2e8c34d2c02f9314c330602e1830",
|
||||||
|
"reference": "73748df28a9c2e8c34d2c02f9314c330602e1830",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"filament/support": "self.version",
|
||||||
|
"php": "^8.2",
|
||||||
|
"spatie/laravel-medialibrary": "^11.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Filament\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Filament support for `spatie/laravel-medialibrary`.",
|
||||||
|
"homepage": "https://github.com/filamentphp/filament",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
|
"source": "https://github.com/filamentphp/filament"
|
||||||
|
},
|
||||||
|
"time": "2025-12-09T09:54:02+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/support",
|
"name": "filament/support",
|
||||||
"version": "v4.4.0",
|
"version": "v4.4.0",
|
||||||
|
|
@ -3535,6 +3649,84 @@
|
||||||
],
|
],
|
||||||
"time": "2025-12-19T02:00:29+00:00"
|
"time": "2025-12-19T02:00:29+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "maennchen/zipstream-php",
|
||||||
|
"version": "3.2.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||||
|
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||||
|
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php-64bit": "^8.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"brianium/paratest": "^7.7",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.86",
|
||||||
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
|
"mikey179/vfsstream": "^1.6",
|
||||||
|
"php-coveralls/php-coveralls": "^2.5",
|
||||||
|
"phpunit/phpunit": "^12.0",
|
||||||
|
"vimeo/psalm": "^6.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"guzzlehttp/psr7": "^2.4",
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ZipStream\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paul Duncan",
|
||||||
|
"email": "pabs@pablotron.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jonatan Männchen",
|
||||||
|
"email": "jonatan@maennchen.ch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jesse Donat",
|
||||||
|
"email": "donatj@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "András Kolesár",
|
||||||
|
"email": "kolesar@kolesar.hu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||||
|
"keywords": [
|
||||||
|
"stream",
|
||||||
|
"zip"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||||
|
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/maennchen",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-10T09:58:31+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "masterminds/html5",
|
"name": "masterminds/html5",
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
|
|
@ -3604,16 +3796,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.9.0",
|
"version": "3.10.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Seldaek/monolog.git",
|
"url": "https://github.com/Seldaek/monolog.git",
|
||||||
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
|
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
|
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
||||||
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
|
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -3631,7 +3823,7 @@
|
||||||
"graylog2/gelf-php": "^1.4.2 || ^2.0",
|
"graylog2/gelf-php": "^1.4.2 || ^2.0",
|
||||||
"guzzlehttp/guzzle": "^7.4.5",
|
"guzzlehttp/guzzle": "^7.4.5",
|
||||||
"guzzlehttp/psr7": "^2.2",
|
"guzzlehttp/psr7": "^2.2",
|
||||||
"mongodb/mongodb": "^1.8",
|
"mongodb/mongodb": "^1.8 || ^2.0",
|
||||||
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
||||||
"php-console/php-console": "^3.1.8",
|
"php-console/php-console": "^3.1.8",
|
||||||
"phpstan/phpstan": "^2",
|
"phpstan/phpstan": "^2",
|
||||||
|
|
@ -3691,7 +3883,7 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/Seldaek/monolog/issues",
|
"issues": "https://github.com/Seldaek/monolog/issues",
|
||||||
"source": "https://github.com/Seldaek/monolog/tree/3.9.0"
|
"source": "https://github.com/Seldaek/monolog/tree/3.10.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -3703,7 +3895,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-03-24T10:02:05+00:00"
|
"time": "2026-01-02T08:56:05+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nesbot/carbon",
|
"name": "nesbot/carbon",
|
||||||
|
|
@ -5382,6 +5574,134 @@
|
||||||
],
|
],
|
||||||
"time": "2022-12-17T21:53:22+00:00"
|
"time": "2022-12-17T21:53:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/image",
|
||||||
|
"version": "3.8.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/image.git",
|
||||||
|
"reference": "4d35db207c4b317bc221d02ab7ba94aa78b44c24"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/image/zipball/4d35db207c4b317bc221d02ab7ba94aa78b44c24",
|
||||||
|
"reference": "4d35db207c4b317bc221d02ab7ba94aa78b44c24",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-exif": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^8.2",
|
||||||
|
"spatie/image-optimizer": "^1.7.5",
|
||||||
|
"spatie/temporary-directory": "^2.2",
|
||||||
|
"symfony/process": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-imagick": "*",
|
||||||
|
"laravel/sail": "^1.34",
|
||||||
|
"pestphp/pest": "^3.0|^4.0",
|
||||||
|
"phpstan/phpstan": "^1.10.50",
|
||||||
|
"spatie/pest-plugin-snapshots": "^2.1",
|
||||||
|
"spatie/pixelmatch-php": "^1.0",
|
||||||
|
"spatie/ray": "^1.40.1",
|
||||||
|
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\Image\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Manipulate images with an expressive API",
|
||||||
|
"homepage": "https://github.com/spatie/image",
|
||||||
|
"keywords": [
|
||||||
|
"image",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/spatie/image/tree/3.8.7"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://spatie.be/open-source/support-us",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-24T15:10:50+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/image-optimizer",
|
||||||
|
"version": "1.8.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/image-optimizer.git",
|
||||||
|
"reference": "2ad9ac7c19501739183359ae64ea6c15869c23d9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/image-optimizer/zipball/2ad9ac7c19501739183359ae64ea6c15869c23d9",
|
||||||
|
"reference": "2ad9ac7c19501739183359ae64ea6c15869c23d9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"php": "^7.3|^8.0",
|
||||||
|
"psr/log": "^1.0 | ^2.0 | ^3.0",
|
||||||
|
"symfony/process": "^4.2|^5.0|^6.0|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"pestphp/pest": "^1.21|^2.0|^3.0|^4.0",
|
||||||
|
"phpunit/phpunit": "^8.5.21|^9.4.4|^10.0|^11.0|^12.0",
|
||||||
|
"symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\ImageOptimizer\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Easily optimize images using PHP",
|
||||||
|
"homepage": "https://github.com/spatie/image-optimizer",
|
||||||
|
"keywords": [
|
||||||
|
"image-optimizer",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/image-optimizer/issues",
|
||||||
|
"source": "https://github.com/spatie/image-optimizer/tree/1.8.1"
|
||||||
|
},
|
||||||
|
"time": "2025-11-26T10:57:19+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/invade",
|
"name": "spatie/invade",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|
@ -5441,6 +5761,116 @@
|
||||||
],
|
],
|
||||||
"time": "2024-05-17T09:06:10+00:00"
|
"time": "2024-05-17T09:06:10+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/laravel-medialibrary",
|
||||||
|
"version": "11.17.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/laravel-medialibrary.git",
|
||||||
|
"reference": "237f34f70ae97523c1a99cad7176e229b8d6f0b6"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/237f34f70ae97523c1a99cad7176e229b8d6f0b6",
|
||||||
|
"reference": "237f34f70ae97523c1a99cad7176e229b8d6f0b6",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer/semver": "^3.4",
|
||||||
|
"ext-exif": "*",
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/bus": "^10.2|^11.0|^12.0",
|
||||||
|
"illuminate/conditionable": "^10.2|^11.0|^12.0",
|
||||||
|
"illuminate/console": "^10.2|^11.0|^12.0",
|
||||||
|
"illuminate/database": "^10.2|^11.0|^12.0",
|
||||||
|
"illuminate/pipeline": "^10.2|^11.0|^12.0",
|
||||||
|
"illuminate/support": "^10.2|^11.0|^12.0",
|
||||||
|
"maennchen/zipstream-php": "^3.1",
|
||||||
|
"php": "^8.2",
|
||||||
|
"spatie/image": "^3.3.2",
|
||||||
|
"spatie/laravel-package-tools": "^1.16.1",
|
||||||
|
"spatie/temporary-directory": "^2.2",
|
||||||
|
"symfony/console": "^6.4.1|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"php-ffmpeg/php-ffmpeg": "<0.6.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"aws/aws-sdk-php": "^3.293.10",
|
||||||
|
"ext-imagick": "*",
|
||||||
|
"ext-pdo_sqlite": "*",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"guzzlehttp/guzzle": "^7.8.1",
|
||||||
|
"larastan/larastan": "^2.7|^3.0",
|
||||||
|
"league/flysystem-aws-s3-v3": "^3.22",
|
||||||
|
"mockery/mockery": "^1.6.7",
|
||||||
|
"orchestra/testbench": "^8.36|^9.15|^10.8",
|
||||||
|
"pestphp/pest": "^2.36|^3.0|^4.0",
|
||||||
|
"phpstan/extension-installer": "^1.3.1",
|
||||||
|
"spatie/laravel-ray": "^1.33",
|
||||||
|
"spatie/pdf-to-image": "^2.2|^3.0",
|
||||||
|
"spatie/pest-expectations": "^1.13",
|
||||||
|
"spatie/pest-plugin-snapshots": "^2.1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"league/flysystem-aws-s3-v3": "Required to use AWS S3 file storage",
|
||||||
|
"php-ffmpeg/php-ffmpeg": "Required for generating video thumbnails",
|
||||||
|
"spatie/pdf-to-image": "Required for generating thumbnails of PDFs and SVGs"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Spatie\\MediaLibrary\\MediaLibraryServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\MediaLibrary\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Associate files with Eloquent models",
|
||||||
|
"homepage": "https://github.com/spatie/laravel-medialibrary",
|
||||||
|
"keywords": [
|
||||||
|
"cms",
|
||||||
|
"conversion",
|
||||||
|
"downloads",
|
||||||
|
"images",
|
||||||
|
"laravel",
|
||||||
|
"laravel-medialibrary",
|
||||||
|
"media",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/laravel-medialibrary/issues",
|
||||||
|
"source": "https://github.com/spatie/laravel-medialibrary/tree/11.17.7"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://spatie.be/open-source/support-us",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-15T08:51:55+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-package-tools",
|
"name": "spatie/laravel-package-tools",
|
||||||
"version": "1.92.7",
|
"version": "1.92.7",
|
||||||
|
|
@ -5567,6 +5997,67 @@
|
||||||
],
|
],
|
||||||
"time": "2025-02-21T14:16:57+00:00"
|
"time": "2025-02-21T14:16:57+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/temporary-directory",
|
||||||
|
"version": "2.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/temporary-directory.git",
|
||||||
|
"reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b",
|
||||||
|
"reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\TemporaryDirectory\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Alex Vanderbist",
|
||||||
|
"email": "alex@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Easily create, use and destroy temporary directories",
|
||||||
|
"homepage": "https://github.com/spatie/temporary-directory",
|
||||||
|
"keywords": [
|
||||||
|
"php",
|
||||||
|
"spatie",
|
||||||
|
"temporary-directory"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/temporary-directory/issues",
|
||||||
|
"source": "https://github.com/spatie/temporary-directory/tree/2.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://spatie.be/open-source/support-us",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-01-13T13:04:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.0",
|
||||||
|
|
|
||||||
303
config/media-library.php
Normal file
303
config/media-library.php
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The disk on which to store added files and derived images by default. Choose
|
||||||
|
* one or more of the disks you've configured in config/filesystems.php.
|
||||||
|
*/
|
||||||
|
'disk_name' => env('MEDIA_DISK', 'public'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The maximum file size of an item in bytes.
|
||||||
|
* Adding a larger file will result in an exception.
|
||||||
|
*/
|
||||||
|
'max_file_size' => 1024 * 1024 * 10, // 10MB
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This queue connection will be used to generate derived and responsive images.
|
||||||
|
* Leave empty to use the default queue connection.
|
||||||
|
*/
|
||||||
|
'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This queue will be used to generate derived and responsive images.
|
||||||
|
* Leave empty to use the default queue.
|
||||||
|
*/
|
||||||
|
'queue_name' => env('MEDIA_QUEUE', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default all conversions will be performed on a queue.
|
||||||
|
*/
|
||||||
|
'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Should database transactions be run after database commits?
|
||||||
|
*/
|
||||||
|
'queue_conversions_after_database_commit' => env('QUEUE_CONVERSIONS_AFTER_DB_COMMIT', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The fully qualified class name of the media model.
|
||||||
|
*/
|
||||||
|
'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The fully qualified class name of the media observer.
|
||||||
|
*/
|
||||||
|
'media_observer' => Spatie\MediaLibrary\MediaCollections\Models\Observers\MediaObserver::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When enabled, media collections will be serialised using the default
|
||||||
|
* laravel model serialization behaviour.
|
||||||
|
*
|
||||||
|
* Keep this option disabled if using Media Library Pro components (https://medialibrary.pro)
|
||||||
|
*/
|
||||||
|
'use_default_collection_serialization' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The fully qualified class name of the model used for temporary uploads.
|
||||||
|
*
|
||||||
|
* This model is only used in Media Library Pro (https://medialibrary.pro)
|
||||||
|
*/
|
||||||
|
'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When enabled, Media Library Pro will only process temporary uploads that were uploaded
|
||||||
|
* in the same session. You can opt to disable this for stateless usage of
|
||||||
|
* the pro components.
|
||||||
|
*/
|
||||||
|
'enable_temporary_uploads_session_affinity' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When enabled, Media Library pro will generate thumbnails for uploaded file.
|
||||||
|
*/
|
||||||
|
'generate_thumbnails_for_temporary_uploads' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is the class that is responsible for naming generated files.
|
||||||
|
*/
|
||||||
|
'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class that contains the strategy for determining a media file's path.
|
||||||
|
*/
|
||||||
|
'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class that contains the strategy for determining how to remove files.
|
||||||
|
*/
|
||||||
|
'file_remover_class' => Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Here you can specify which path generator should be used for the given class.
|
||||||
|
*/
|
||||||
|
'custom_path_generators' => [
|
||||||
|
// Model::class => PathGenerator::class
|
||||||
|
// or
|
||||||
|
// 'model_morph_alias' => PathGenerator::class
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When urls to files get generated, this class will be called. Use the default
|
||||||
|
* if your files are stored locally above the site root or on s3.
|
||||||
|
*/
|
||||||
|
'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Moves media on updating to keep path consistent. Enable it only with a custom
|
||||||
|
* PathGenerator that uses, for example, the media UUID.
|
||||||
|
*/
|
||||||
|
'moves_media_on_update' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Whether to activate versioning when urls to files get generated.
|
||||||
|
* When activated, this attaches a ?v=xx query string to the URL.
|
||||||
|
*/
|
||||||
|
'version_urls' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The media library will try to optimize all converted images by removing
|
||||||
|
* metadata and applying a little bit of compression. These are
|
||||||
|
* the optimizers that will be used by default.
|
||||||
|
*/
|
||||||
|
'image_optimizers' => [
|
||||||
|
Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [
|
||||||
|
'-m85', // set maximum quality to 85%
|
||||||
|
'--force', // ensure that progressive generation is always done also if a little bigger
|
||||||
|
'--strip-all', // this strips out all text information such as comments and EXIF data
|
||||||
|
'--all-progressive', // this will make sure the resulting image is a progressive one
|
||||||
|
],
|
||||||
|
Spatie\ImageOptimizer\Optimizers\Pngquant::class => [
|
||||||
|
'--force', // required parameter for this package
|
||||||
|
],
|
||||||
|
Spatie\ImageOptimizer\Optimizers\Optipng::class => [
|
||||||
|
'-i0', // this will result in a non-interlaced, progressive scanned image
|
||||||
|
'-o2', // this set the optimization level to two (multiple IDAT compression trials)
|
||||||
|
'-quiet', // required parameter for this package
|
||||||
|
],
|
||||||
|
Spatie\ImageOptimizer\Optimizers\Svgo::class => [
|
||||||
|
'--disable=cleanupIDs', // disabling because it is known to cause troubles
|
||||||
|
],
|
||||||
|
Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [
|
||||||
|
'-b', // required parameter for this package
|
||||||
|
'-O3', // this produces the slowest but best results
|
||||||
|
],
|
||||||
|
Spatie\ImageOptimizer\Optimizers\Cwebp::class => [
|
||||||
|
'-m 6', // for the slowest compression method in order to get the best compression.
|
||||||
|
'-pass 10', // for maximizing the amount of analysis pass.
|
||||||
|
'-mt', // multithreading for some speed improvements.
|
||||||
|
'-q 90', // quality factor that brings the least noticeable changes.
|
||||||
|
],
|
||||||
|
Spatie\ImageOptimizer\Optimizers\Avifenc::class => [
|
||||||
|
'-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63).
|
||||||
|
'-j all', // number of jobs (worker threads, "all" uses all available cores).
|
||||||
|
'--min 0', // min quantizer for color (0-63).
|
||||||
|
'--max 63', // max quantizer for color (0-63).
|
||||||
|
'--minalpha 0', // min quantizer for alpha (0-63).
|
||||||
|
'--maxalpha 63', // max quantizer for alpha (0-63).
|
||||||
|
'-a end-usage=q', // rate control mode set to Constant Quality mode.
|
||||||
|
'-a tune=ssim', // SSIM as tune the encoder for distortion metric.
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These generators will be used to create an image of media files.
|
||||||
|
*/
|
||||||
|
'image_generators' => [
|
||||||
|
Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class,
|
||||||
|
Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class,
|
||||||
|
Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class,
|
||||||
|
Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class,
|
||||||
|
Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class,
|
||||||
|
Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The path where to store temporary files while performing image conversions.
|
||||||
|
* If set to null, storage_path('media-library/temp') will be used.
|
||||||
|
*/
|
||||||
|
'temporary_directory_path' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The engine that should perform the image conversions.
|
||||||
|
* Should be either `gd` or `imagick`.
|
||||||
|
*/
|
||||||
|
'image_driver' => env('IMAGE_DRIVER', 'gd'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FFMPEG & FFProbe binaries paths, only used if you try to generate video
|
||||||
|
* thumbnails and have installed the php-ffmpeg/php-ffmpeg composer
|
||||||
|
* dependency.
|
||||||
|
*/
|
||||||
|
'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
|
||||||
|
'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The timeout (in seconds) that will be used when generating video
|
||||||
|
* thumbnails via FFMPEG.
|
||||||
|
*/
|
||||||
|
'ffmpeg_timeout' => env('FFMPEG_TIMEOUT', 900),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The number of threads that FFMPEG should use. 0 means that FFMPEG
|
||||||
|
* may decide itself.
|
||||||
|
*/
|
||||||
|
'ffmpeg_threads' => env('FFMPEG_THREADS', 0),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Here you can override the class names of the jobs used by this package. Make sure
|
||||||
|
* your custom jobs extend the ones provided by the package.
|
||||||
|
*/
|
||||||
|
'jobs' => [
|
||||||
|
'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class,
|
||||||
|
'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the addMediaFromUrl method you may want to replace the default downloader.
|
||||||
|
* This is particularly useful when the url of the image is behind a firewall and
|
||||||
|
* need to add additional flags, possibly using curl.
|
||||||
|
*/
|
||||||
|
'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the addMediaFromUrl method the SSL is verified by default.
|
||||||
|
* This is option disables SSL verification when downloading remote media.
|
||||||
|
* Please note that this is a security risk and should only be false in a local environment.
|
||||||
|
*/
|
||||||
|
'media_downloader_ssl' => env('MEDIA_DOWNLOADER_SSL', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The default lifetime in minutes for temporary urls.
|
||||||
|
* This is used when you call the `getLastTemporaryUrl` or `getLastTemporaryUrl` method on a media item.
|
||||||
|
*/
|
||||||
|
'temporary_url_default_lifetime' => env('MEDIA_TEMPORARY_URL_DEFAULT_LIFETIME', 5),
|
||||||
|
|
||||||
|
'remote' => [
|
||||||
|
/*
|
||||||
|
* Any extra headers that should be included when uploading media to
|
||||||
|
* a remote disk. Even though supported headers may vary between
|
||||||
|
* different drivers, a sensible default has been provided.
|
||||||
|
*
|
||||||
|
* Supported by S3: CacheControl, Expires, StorageClass,
|
||||||
|
* ServerSideEncryption, Metadata, ACL, ContentEncoding
|
||||||
|
*/
|
||||||
|
'extra_headers' => [
|
||||||
|
'CacheControl' => 'max-age=604800',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'responsive_images' => [
|
||||||
|
/*
|
||||||
|
* This class is responsible for calculating the target widths of the responsive
|
||||||
|
* images. By default we optimize for filesize and create variations that each are 30%
|
||||||
|
* smaller than the previous one. More info in the documentation.
|
||||||
|
*
|
||||||
|
* https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images
|
||||||
|
*/
|
||||||
|
'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default rendering media to a responsive image will add some javascript and a tiny placeholder.
|
||||||
|
* This ensures that the browser can already determine the correct layout.
|
||||||
|
* When disabled, no tiny placeholder is generated.
|
||||||
|
*/
|
||||||
|
'use_tiny_placeholders' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This class will generate the tiny placeholder used for progressive image loading. By default
|
||||||
|
* the media library will use a tiny blurred jpg image.
|
||||||
|
*/
|
||||||
|
'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When enabling this option, a route will be registered that will enable
|
||||||
|
* the Media Library Pro Vue and React components to move uploaded files
|
||||||
|
* in a S3 bucket to their right place.
|
||||||
|
*/
|
||||||
|
'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When converting Media instances to response the media library will add
|
||||||
|
* a `loading` attribute to the `img` tag. Here you can set the default
|
||||||
|
* value of that attribute.
|
||||||
|
*
|
||||||
|
* Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction.
|
||||||
|
*
|
||||||
|
* More info: https://css-tricks.com/native-lazy-loading/
|
||||||
|
*/
|
||||||
|
'default_loading_attribute_value' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You can specify a prefix for that is used for storing all media.
|
||||||
|
* If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory.
|
||||||
|
*/
|
||||||
|
'prefix' => env('MEDIA_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When forcing lazy loading, media will be loaded even if you don't eager load media and you have
|
||||||
|
* disabled lazy loading globally in the service provider.
|
||||||
|
*/
|
||||||
|
'force_lazy_loading' => env('FORCE_MEDIA_LIBRARY_LAZY_LOADING', true),
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('entries', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->boolean('is_published')->default(false);
|
||||||
|
$table->boolean('is_featured')->default(false);
|
||||||
|
$table->date('published_at')->nullable();
|
||||||
|
$table->mediumText('content')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('entries');
|
||||||
|
}
|
||||||
|
};
|
||||||
32
database/migrations/2026_01_02_160151_create_media_table.php
Normal file
32
database/migrations/2026_01_02_160151_create_media_table.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('media', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->morphs('model');
|
||||||
|
$table->uuid()->nullable()->unique();
|
||||||
|
$table->string('collection_name');
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('file_name');
|
||||||
|
$table->string('mime_type')->nullable();
|
||||||
|
$table->string('disk');
|
||||||
|
$table->string('conversions_disk')->nullable();
|
||||||
|
$table->unsignedBigInteger('size');
|
||||||
|
$table->json('manipulations');
|
||||||
|
$table->json('custom_properties');
|
||||||
|
$table->json('generated_conversions');
|
||||||
|
$table->json('responsive_images');
|
||||||
|
$table->unsignedInteger('order_column')->nullable()->index();
|
||||||
|
|
||||||
|
$table->nullableTimestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -12,4 +12,3 @@ It is a way to create apps that have all the features of an advanced app with ma
|
||||||
|
|
||||||
There are so many things to do, we don't have the time to re-write or re-invent what is already achieved by filament and likely, better than we could create ourselves or likely dream of.
|
There are so many things to do, we don't have the time to re-write or re-invent what is already achieved by filament and likely, better than we could create ourselves or likely dream of.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
86
docs/decisions/003-initial-model-and-filament-resource.md
Normal file
86
docs/decisions/003-initial-model-and-filament-resource.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# 2026-01-02
|
||||||
|
|
||||||
|
## Entry model
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan make:model -m Entry
|
||||||
|
```
|
||||||
|
|
||||||
|
creates skeleton files for a model and migration
|
||||||
|
|
||||||
|
we edit the migration to hold the fields we desire to be present in each record
|
||||||
|
|
||||||
|
just the minimal are added for now, paying attention to the amount of data that these may need to hold
|
||||||
|
|
||||||
|
* string (varchar): ~255 chars default.
|
||||||
|
* text: ~65 KB.
|
||||||
|
* mediumText: ~16 MB.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
runs the migration to create the table
|
||||||
|
|
||||||
|
## Entry filament resource
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ php artisan filament:resource Entry
|
||||||
|
|
||||||
|
The "title attribute" is used to label each record in the UI.
|
||||||
|
|
||||||
|
You can leave this blank if records do not have a title.
|
||||||
|
|
||||||
|
┌ What is the title attribute for this model? ─────────────────┐
|
||||||
|
│ title │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌ Would you like to generate a read-only view page for the resource? ┐
|
||||||
|
│ Yes │
|
||||||
|
└────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌ Should the configuration be generated from the current database columns? ┐
|
||||||
|
│ Yes │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
INFO Filament resource [App\Filament\Resources\Entries\EntryResource] created successfully.
|
||||||
|
```
|
||||||
|
|
||||||
|
There is more work to do on this model but for now, a new resource is added to the admin panel by which records can be added, edited, listed.
|
||||||
|
|
||||||
|
Full CRUD is already possible. This is amazing if you think how long this would have taken to create all this manually.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### slugify
|
||||||
|
|
||||||
|
```php
|
||||||
|
TextInput::make('title')
|
||||||
|
->required()
|
||||||
|
->reactive()
|
||||||
|
->afterStateUpdated(function ($state, $set): void {
|
||||||
|
$set('slug', Str::slug((string) $state));
|
||||||
|
}),
|
||||||
|
TextInput::make('slug')
|
||||||
|
->required(),
|
||||||
|
```
|
||||||
|
|
||||||
|
adding reactive and afterStateUpdted to title automatically creates a safe slug
|
||||||
|
|
||||||
|
I class things like this creature comforts of the framework and it shows how simple it can be to make fields active and updated by previous entries
|
||||||
|
|
||||||
|
For the user, this reduces error and confusion over what a url should be
|
||||||
|
|
||||||
|
### rich editor
|
||||||
|
|
||||||
|
just changing the TextInput for content
|
||||||
|
|
||||||
|
```php
|
||||||
|
RichEditor::make('content')
|
||||||
|
->columnSpanFull(),
|
||||||
|
```
|
||||||
|
|
||||||
|
to `RichEditr` gives us a 'tiptap' rich text editor
|
||||||
|
|
||||||
|
already, this is feeling more like a mini CMS, with relatively little effort
|
||||||
|
|
||||||
10
docs/decisions/004-spatie-media-library
Normal file
10
docs/decisions/004-spatie-media-library
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# 2026-01-02
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan storage:link
|
||||||
|
composer require "spatie/laravel-medialibrary"
|
||||||
|
composer require filament/spatie-laravel-media-library-plugin:"^4.0" -W
|
||||||
|
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -0,0 +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');
|
||||||
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>
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
pest()->extend(Tests\TestCase::class)
|
pest()->extend(Tests\TestCase::class)
|
||||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||||
->in('Feature');
|
->in('Feature', 'Unit');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,15 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
abstract class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
//
|
/**
|
||||||
|
* Creates the application.
|
||||||
|
*/
|
||||||
|
public function createApplication(): \Illuminate\Foundation\Application
|
||||||
|
{
|
||||||
|
$app = require __DIR__ . '/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
tests/Unit/EntryModelTest.php
Normal file
19
tests/Unit/EntryModelTest.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
|
||||||
|
it('has correct fillable attributes', function () {
|
||||||
|
$expected = [
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'is_published',
|
||||||
|
'is_featured',
|
||||||
|
'published_at',
|
||||||
|
'content',
|
||||||
|
];
|
||||||
|
|
||||||
|
$entry = new Entry();
|
||||||
|
|
||||||
|
expect($entry->getFillable())->toEqual($expected);
|
||||||
|
});
|
||||||
53
tests/Unit/EntryResourceTest.php
Normal file
53
tests/Unit/EntryResourceTest.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Resources\Entries\EntryResource;
|
||||||
|
use App\Models\Entry;
|
||||||
|
|
||||||
|
it('references the correct model and record title attribute', function () {
|
||||||
|
$defaults = (new ReflectionClass(EntryResource::class))->getDefaultProperties();
|
||||||
|
|
||||||
|
expect($defaults['model'] ?? null)->toBe(Entry::class);
|
||||||
|
expect($defaults['recordTitleAttribute'] ?? null)->toBe('title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines the expected pages', function () {
|
||||||
|
$pages = EntryResource::getPages();
|
||||||
|
|
||||||
|
expect(array_keys($pages))->toEqual(['index', 'create', 'view', 'edit']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts new record when entered via the form schema', function () {
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'title' => 'Test Entry',
|
||||||
|
'slug' => 'test-entry',
|
||||||
|
'description' => 'This is a test entry.',
|
||||||
|
'is_published' => true,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => now()->toDateString(),
|
||||||
|
'content' => '<p>This is the content of the test entry.</p>',
|
||||||
|
];
|
||||||
|
|
||||||
|
$entry = new Entry();
|
||||||
|
$entry->fill($data);
|
||||||
|
|
||||||
|
expect($entry->slug)->toBe('test-entry');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a record correctly', function () {
|
||||||
|
$entry = Entry::create([
|
||||||
|
'title' => 'Test Entry to Delete',
|
||||||
|
'slug' => 'test-entry-to-delete',
|
||||||
|
'description' => 'This is a test entry.',
|
||||||
|
'is_published' => false,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => null,
|
||||||
|
'content' => '<p>This is the content of the test entry.</p>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entryId = $entry->id;
|
||||||
|
$entry->delete();
|
||||||
|
|
||||||
|
$deletedEntry = Entry::find($entryId);
|
||||||
|
expect($deletedEntry)->toBeNull();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue