share-lt/app/Filament/Resources/Entries/Schemas/EntryForm.php

388 lines
21 KiB
PHP
Raw Normal View History

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