feat: add category management

associate with entries and text widgets
This commit is contained in:
jon brookes 2026-01-09 13:18:37 +00:00
parent c83028b4d4
commit 9b9e1a8e29
22 changed files with 392 additions and 46 deletions

View file

@ -0,0 +1,50 @@
<?php
namespace App\Filament\Resources\Categroys;
use App\Filament\Resources\Categroys\Pages\CreateCategroy;
use App\Filament\Resources\Categroys\Pages\EditCategroy;
use App\Filament\Resources\Categroys\Pages\ListCategroys;
use App\Filament\Resources\Categroys\Schemas\CategroyForm;
use App\Filament\Resources\Categroys\Tables\CategroysTable;
use App\Models\Category;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class CategroyResource extends Resource
{
protected static ?string $model = Category::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::RectangleGroup;
protected static ?string $recordTitleAttribute = 'name';
public static function form(Schema $schema): Schema
{
return CategroyForm::configure($schema);
}
public static function table(Table $table): Table
{
return CategroysTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListCategroys::route('/'),
'create' => CreateCategroy::route('/create'),
'edit' => EditCategroy::route('/{record}/edit'),
];
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Categroys\Pages;
use App\Filament\Resources\Categroys\CategroyResource;
use Filament\Resources\Pages\CreateRecord;
class CreateCategroy extends CreateRecord
{
protected static string $resource = CategroyResource::class;
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Categroys\Pages;
use App\Filament\Resources\Categroys\CategroyResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditCategroy extends EditRecord
{
protected static string $resource = CategroyResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Categroys\Pages;
use App\Filament\Resources\Categroys\CategroyResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListCategroys extends ListRecords
{
protected static string $resource = CategroyResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Categroys\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class CategroyForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Category Name')
->required()
->maxLength(255),
]);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Resources\Categroys\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class CategroysTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Category Name')
->sortable()
->searchable(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View file

@ -2,19 +2,19 @@
namespace App\Filament\Resources\Entries\Schemas;
use App\Models\Category;
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\SpatieTagsInput;
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;
use Filament\Forms\Components\SpatieTagsInput;
class EntryForm
{
@ -66,13 +66,13 @@ class EntryForm
$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>";
$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) {
@ -86,39 +86,42 @@ class EntryForm
])
->action(function (array $data, SpatieMediaLibraryFileUpload $component): void {
$record = $component->getRecord();
if (!$record) {
if (! $record) {
\Filament\Notifications\Notification::make()
->warning()
->title('Save the entry first')
->send();
return;
}
if (!$data['image_id']) {
if (! $data['image_id']) {
return;
}
$sourceMedia = Media::find($data['image_id']);
if (!$sourceMedia || !file_exists($sourceMedia->getPath())) {
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;
$tempCopy = sys_get_temp_dir().'/'.uniqid().'_'.$sourceMedia->file_name;
copy($sourceFile, $tempCopy);
try {
// Verify record has ID
if (!$record->id) {
if (! $record->id) {
\Filament\Notifications\Notification::make()
->danger()
->title('Entry must be saved first')
->send();
return;
}
@ -138,7 +141,7 @@ class EntryForm
} catch (\Exception $e) {
\Filament\Notifications\Notification::make()
->danger()
->title('Error: ' . $e->getMessage())
->title('Error: '.$e->getMessage())
->send();
} finally {
if (file_exists($tempCopy)) {
@ -152,6 +155,14 @@ class EntryForm
Toggle::make('is_featured')
->required(),
DatePicker::make('published_at'),
Select::make('category_id')
->label('Category')
->options(function () {
return Category::all()
->pluck('name', 'id')
->toArray();
})
->searchable(),
RichEditor::make('content')
->columnSpanFull()
->hintAction(
@ -177,17 +188,17 @@ class EntryForm
// 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>";
$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();

View file

@ -2,6 +2,8 @@
namespace App\Filament\Resources\TextWidgets\Schemas;
use App\Models\Category;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
@ -20,6 +22,14 @@ class TextWidgetForm
Textarea::make('content')
->rows(5)
->columnSpanFull(),
Select::make('category_id')
->label('Category')
->options(function () {
return Category::all()
->pluck('name', 'id')
->toArray();
})
->searchable(),
]);
}