Files
landing-api/app/Filament/Resources/Projects/RelationManagers/GalleryRelationManager.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

143 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Filament\Resources\Projects\RelationManagers;
use App\Filament\Support\TranslatableTabs;
use App\Models\ImageAttachment;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* The gallery of a project.
*
* A table rather than a repeater inside the form, because the thing you need
* to see while editing a gallery is the images themselves, and because the
* order matters — it is dragged here rather than typed into a number field.
*/
class GalleryRelationManager extends RelationManager
{
protected static string $relationship = 'images';
public static function getTitle(Model $ownerRecord, string $pageClass): string
{
return __('panel.gallery.title');
}
public static function getBadge(Model $ownerRecord, string $pageClass): ?string
{
$count = $ownerRecord->images()->count();
return $count > 0 ? (string) $count : null;
}
public function form(Schema $schema): Schema
{
return $schema->components([
FileUpload::make('path')
->label(__('panel.gallery.fields.image'))
->image()
// Explicit: the default disk is private, and the frontend runs
// on another host.
->disk(config('media.disk'))
->directory('projects/gallery')
->imageEditor()
->required()
->columnSpanFull(),
TranslatableTabs::make(fn (string $locale) => [
TextInput::make("alt.{$locale}")
->label(__('panel.gallery.fields.alt'))
->helperText(__('panel.gallery.fields.alt_hint'))
->maxLength(255)
->columnSpanFull(),
// Shown to everyone, unlike the alt text. "Before" and
// "after" belong here.
TextInput::make("caption.{$locale}")
->label(__('panel.gallery.fields.caption'))
->helperText(__('panel.gallery.fields.caption_hint'))
->maxLength(255)
->columnSpanFull(),
])->columnSpanFull(),
]);
}
public function table(Table $table): Table
{
$locale = app()->getLocale();
return $table
->defaultSort('position')
->reorderable('position')
// The table shows each image's real dimensions; loaded up front so
// that costs one query rather than one per row.
->modifyQueryUsing(fn (Builder $query) => $query->with('media'))
->columns([
ImageColumn::make('path')
->label('')
->disk(config('media.disk'))
->height(56),
TextColumn::make('caption')
->label(__('panel.gallery.fields.caption'))
->state(fn (ImageAttachment $record) => $record->getTranslation('caption', $locale, false) ?: null)
->placeholder('—')
->wrap(),
TextColumn::make('alt')
->label(__('panel.gallery.fields.alt'))
->state(fn (ImageAttachment $record) => $record->getTranslation('alt', $locale, false) ?: null)
// An image nobody described is the one worth spotting from
// the listing.
->placeholder(__('panel.gallery.missing_alt'))
->color(fn (ImageAttachment $record) => $record->getTranslation('alt', $locale, false)
? null
: 'warning')
->wrap()
->toggleable(),
TextColumn::make('media.width')
->label(__('panel.gallery.fields.size'))
->state(fn (ImageAttachment $record) => $record->media?->width
? "{$record->media->width}×{$record->media->height}"
: __('panel.gallery.pending'))
->badge()
->color(fn (ImageAttachment $record) => $record->media?->width ? 'gray' : 'warning')
->toggleable(),
])
->headerActions([
CreateAction::make()
->label(__('panel.gallery.actions.add'))
// New images go at the end rather than jumping to the top.
->mutateDataUsing(function (array $data): array {
$data['position'] = (int) $this->getOwnerRecord()->images()->max('position') + 1;
return $data;
}),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
DeleteBulkAction::make(),
])
->emptyStateHeading(__('panel.gallery.empty.heading'))
->emptyStateDescription(__('panel.gallery.empty.description'))
->emptyStateIcon('heroicon-o-photo');
}
}