Tag blog posts and sign them with a member
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / test-postgres (push) Has been cancelled
Tests / mcp (push) Has been cancelled

Tags are named in every language and made from the page form; the byline is
the member who signs a post on the site. The public API sends both, filters
posts by tag and lists the tags in use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 11:01:18 +02:00
parent 6fc600e6a1
commit 9f970808ca
27 changed files with 809 additions and 12 deletions

View File

@@ -9,6 +9,7 @@ use App\Filament\Resources\Pages\PageResource;
use App\Filament\Support\TranslatableTabs;
use App\Filament\Support\TranslatedOptionLabel;
use App\Models\Page;
use App\Models\Tag;
use App\Rules\UniqueTranslatedSlug;
use App\Support\DuplicateTitles;
use Filament\Forms\Components\DateTimePicker;
@@ -126,6 +127,39 @@ class PageForm
->searchable()
->preload(),
// What the post is about, for the blog to be read by
// subject. A missing one is made from here.
Select::make('tags')
->label(__('panel.tags.plural'))
->relationship('tags', 'name->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('name'))
->multiple()
->searchable()
->preload()
->createOptionForm([
TextInput::make('name')
->label(__('panel.tags.fields.name'))
->required()
->maxLength(80),
])
->createOptionUsing(function (array $data): int {
$locale = (string) config('locales.default');
return Tag::query()->create([
'name' => [$locale => $data['name']],
'slug' => [$locale => Str::slug($data['name'])],
])->getKey();
})
->createOptionAction(fn ($action) => $action->authorize('create', Tag::class)),
// Who signs it on the site, with a link to their profile.
Select::make('byline_member_id')
->label(__('panel.pages.fields.byline'))
->helperText(__('panel.pages.fields.byline_hint'))
->relationship('byline', 'name')
->searchable()
->preload(),
Select::make('author_id')
->label(__('panel.pages.fields.author'))
->relationship('author', 'name')

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Tags\Pages;
use App\Filament\Resources\Tags\TagResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ManageRecords;
class ManageTags extends ManageRecords
{
protected static string $resource = TagResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Tags;
use App\Filament\Resources\Tags\Pages\ManageTags;
use App\Filament\Support\TranslatableTabs;
use App\Models\Tag;
use App\Rules\UniqueTranslatedSlug;
use BackedEnum;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use UnitEnum;
/**
* Blog tags: what each post is about, named in every language.
*/
class TagResource extends Resource
{
protected static ?string $model = Tag::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedHashtag;
protected static ?int $navigationSort = 3;
public static function getModelLabel(): string
{
return __('panel.tags.label');
}
public static function getPluralModelLabel(): string
{
return __('panel.tags.plural');
}
public static function getNavigationGroup(): string|UnitEnum|null
{
return __('panel.groups.content');
}
public static function getRecordTitle(?Model $record): ?string
{
return $record?->getTranslation('name', app()->getLocale());
}
public static function form(Schema $schema): Schema
{
return $schema->components([
TranslatableTabs::make(fn (string $locale, bool $isDefault) => [
TextInput::make("name.{$locale}")
->label(__('panel.tags.fields.name'))
->required($isDefault)
->maxLength(80)
->live(onBlur: true)
->afterStateUpdated(function (?string $state, callable $get, callable $set) use ($locale): void {
if (blank($get("slug.{$locale}"))) {
$set("slug.{$locale}", Str::slug((string) $state));
}
}),
TextInput::make("slug.{$locale}")
->label(__('panel.tags.fields.slug'))
->required($isDefault)
->maxLength(80)
->rules(fn ($record) => [new UniqueTranslatedSlug(Tag::class, $record?->id)]),
])->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn ($query) => $query->withCount('pages'))
->columns([
TextColumn::make('name')
->label(__('panel.tags.fields.name'))
->searchable(),
TextColumn::make('slug')
->label(__('panel.tags.fields.slug'))
->color('gray'),
TextColumn::make('pages_count')
->label(__('panel.tags.fields.pages'))
->sortable(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
]);
}
public static function getPages(): array
{
return [
'index' => ManageTags::route('/'),
];
}
}