feat: add category management
associate with entries and text widgets
This commit is contained in:
parent
c83028b4d4
commit
9b9e1a8e29
22 changed files with 392 additions and 46 deletions
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
added tags to entry model
|
added tags to entry model
|
||||||
|
|
||||||
|
added text widget and category
|
||||||
|
|
||||||
## 2026-01-07
|
## 2026-01-07
|
||||||
|
|
||||||
added simple API for entries model
|
added simple API for entries model
|
||||||
|
|
|
||||||
50
app/Filament/Resources/Categroys/CategroyResource.php
Normal file
50
app/Filament/Resources/Categroys/CategroyResource.php
Normal 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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Categroys/Pages/CreateCategroy.php
Normal file
11
app/Filament/Resources/Categroys/Pages/CreateCategroy.php
Normal 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;
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Categroys/Pages/EditCategroy.php
Normal file
19
app/Filament/Resources/Categroys/Pages/EditCategroy.php
Normal 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Categroys/Pages/ListCategroys.php
Normal file
19
app/Filament/Resources/Categroys/Pages/ListCategroys.php
Normal 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Filament/Resources/Categroys/Schemas/CategroyForm.php
Normal file
20
app/Filament/Resources/Categroys/Schemas/CategroyForm.php
Normal 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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Filament/Resources/Categroys/Tables/CategroysTable.php
Normal file
34
app/Filament/Resources/Categroys/Tables/CategroysTable.php
Normal 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(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,19 +2,19 @@
|
||||||
|
|
||||||
namespace App\Filament\Resources\Entries\Schemas;
|
namespace App\Filament\Resources\Entries\Schemas;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\RichEditor;
|
use Filament\Forms\Components\RichEditor;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||||
|
use Filament\Forms\Components\SpatieTagsInput;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
use Filament\Forms\Components\SpatieTagsInput;
|
|
||||||
|
|
||||||
class EntryForm
|
class EntryForm
|
||||||
{
|
{
|
||||||
|
|
@ -67,12 +67,12 @@ class EntryForm
|
||||||
$fileName = e($item->file_name);
|
$fileName = e($item->file_name);
|
||||||
$name = e($item->name ?? '');
|
$name = e($item->name ?? '');
|
||||||
|
|
||||||
$html = "<div class='flex items-center gap-3'>" .
|
$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' />" .
|
"<img src='{$url}' class='rounded' style='width:60px;height:60px;object-fit:cover;' alt='{$fileName}' loading='lazy' />".
|
||||||
"<div class='flex flex-col'>" .
|
"<div class='flex flex-col'>".
|
||||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
"<span class='font-medium text-sm'>{$name}</span>".
|
||||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
"<span class='text-xs text-gray-500'>{$fileName}</span>".
|
||||||
"</div></div>";
|
'</div></div>';
|
||||||
|
|
||||||
return [$item->id => $html];
|
return [$item->id => $html];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
@ -87,38 +87,41 @@ class EntryForm
|
||||||
->action(function (array $data, SpatieMediaLibraryFileUpload $component): void {
|
->action(function (array $data, SpatieMediaLibraryFileUpload $component): void {
|
||||||
$record = $component->getRecord();
|
$record = $component->getRecord();
|
||||||
|
|
||||||
if (!$record) {
|
if (! $record) {
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
->warning()
|
->warning()
|
||||||
->title('Save the entry first')
|
->title('Save the entry first')
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$data['image_id']) {
|
if (! $data['image_id']) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceMedia = Media::find($data['image_id']);
|
$sourceMedia = Media::find($data['image_id']);
|
||||||
if (!$sourceMedia || !file_exists($sourceMedia->getPath())) {
|
if (! $sourceMedia || ! file_exists($sourceMedia->getPath())) {
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
->title('Image file not found')
|
->title('Image file not found')
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceFile = $sourceMedia->getPath();
|
$sourceFile = $sourceMedia->getPath();
|
||||||
$tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name;
|
$tempCopy = sys_get_temp_dir().'/'.uniqid().'_'.$sourceMedia->file_name;
|
||||||
copy($sourceFile, $tempCopy);
|
copy($sourceFile, $tempCopy);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify record has ID
|
// Verify record has ID
|
||||||
if (!$record->id) {
|
if (! $record->id) {
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
->title('Entry must be saved first')
|
->title('Entry must be saved first')
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +141,7 @@ class EntryForm
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
->title('Error: ' . $e->getMessage())
|
->title('Error: '.$e->getMessage())
|
||||||
->send();
|
->send();
|
||||||
} finally {
|
} finally {
|
||||||
if (file_exists($tempCopy)) {
|
if (file_exists($tempCopy)) {
|
||||||
|
|
@ -152,6 +155,14 @@ class EntryForm
|
||||||
Toggle::make('is_featured')
|
Toggle::make('is_featured')
|
||||||
->required(),
|
->required(),
|
||||||
DatePicker::make('published_at'),
|
DatePicker::make('published_at'),
|
||||||
|
Select::make('category_id')
|
||||||
|
->label('Category')
|
||||||
|
->options(function () {
|
||||||
|
return Category::all()
|
||||||
|
->pluck('name', 'id')
|
||||||
|
->toArray();
|
||||||
|
})
|
||||||
|
->searchable(),
|
||||||
RichEditor::make('content')
|
RichEditor::make('content')
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->hintAction(
|
->hintAction(
|
||||||
|
|
@ -182,12 +193,12 @@ class EntryForm
|
||||||
$name = e($item->name ?? '');
|
$name = e($item->name ?? '');
|
||||||
|
|
||||||
// Smaller image preview for better performance
|
// Smaller image preview for better performance
|
||||||
$html = "<div class='flex items-center gap-3'>" .
|
$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' />" .
|
"<img src='{$url}' class='rounded' style='width:60px;height:60px;object-fit:cover;' alt='{$fileName}' loading='lazy' />".
|
||||||
"<div class='flex flex-col'>" .
|
"<div class='flex flex-col'>".
|
||||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
"<span class='font-medium text-sm'>{$name}</span>".
|
||||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
"<span class='text-xs text-gray-500'>{$fileName}</span>".
|
||||||
"</div></div>";
|
'</div></div>';
|
||||||
|
|
||||||
return [$url => $html];
|
return [$url => $html];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Filament\Resources\TextWidgets\Schemas;
|
namespace App\Filament\Resources\TextWidgets\Schemas;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
@ -20,6 +22,14 @@ class TextWidgetForm
|
||||||
Textarea::make('content')
|
Textarea::make('content')
|
||||||
->rows(5)
|
->rows(5)
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
Select::make('category_id')
|
||||||
|
->label('Category')
|
||||||
|
->options(function () {
|
||||||
|
return Category::all()
|
||||||
|
->pluck('name', 'id')
|
||||||
|
->toArray();
|
||||||
|
})
|
||||||
|
->searchable(),
|
||||||
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Resources\EntryResource;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -14,7 +15,7 @@ class EntryController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
return Entry::all();
|
return EntryResource::collection(Entry::with('category')->get());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,7 +25,7 @@ class EntryController extends Controller
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
if (!$user || $user->email !== config('app.admin_email')) {
|
if (! $user || $user->email !== config('app.admin_email')) {
|
||||||
return response()->json(['message' => 'Forbidden'], 403);
|
return response()->json(['message' => 'Forbidden'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,13 +36,13 @@ class EntryController extends Controller
|
||||||
|
|
||||||
$validated['slug'] = $this->generateUniqueSlug($validated['title']);
|
$validated['slug'] = $this->generateUniqueSlug($validated['title']);
|
||||||
|
|
||||||
return Entry::create($validated);
|
return new EntryResource(Entry::create($validated));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateUniqueSlug(string $title): string
|
private function generateUniqueSlug(string $title): string
|
||||||
{
|
{
|
||||||
do {
|
do {
|
||||||
$slug = Str::slug($title) . '-' . Str::random(8);
|
$slug = Str::slug($title).'-'.Str::random(8);
|
||||||
} while (Entry::where('slug', $slug)->exists());
|
} while (Entry::where('slug', $slug)->exists());
|
||||||
|
|
||||||
return $slug;
|
return $slug;
|
||||||
|
|
@ -54,7 +55,7 @@ class EntryController extends Controller
|
||||||
{
|
{
|
||||||
$this->authorize('view', $entry);
|
$this->authorize('view', $entry);
|
||||||
|
|
||||||
return $entry;
|
return new EntryResource($entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,7 +70,7 @@ class EntryController extends Controller
|
||||||
|
|
||||||
$entry->update($validated);
|
$entry->update($validated);
|
||||||
|
|
||||||
return $entry;
|
return new EntryResource($entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Resources\TextWidgetResource;
|
||||||
use App\Models\TextWidget;
|
use App\Models\TextWidget;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -13,7 +14,7 @@ class TextWidgetController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
return TextWidget::all();
|
return TextWidget::with('category')->get()->map(fn ($tw) => new TextWidgetResource($tw));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,7 +34,7 @@ class TextWidgetController extends Controller
|
||||||
'content' => 'required|string',
|
'content' => 'required|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return TextWidget::create($validated);
|
return new TextWidgetResource(TextWidget::create($validated));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,7 +42,7 @@ class TextWidgetController extends Controller
|
||||||
*/
|
*/
|
||||||
public function show(TextWidget $textWidget)
|
public function show(TextWidget $textWidget)
|
||||||
{
|
{
|
||||||
return $textWidget;
|
return new TextWidgetResource($textWidget->load('category'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,7 +58,7 @@ class TextWidgetController extends Controller
|
||||||
|
|
||||||
$textWidget->update($validated);
|
$textWidget->update($validated);
|
||||||
|
|
||||||
return $textWidget;
|
return new TextWidgetResource($textWidget->load('category'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
32
app/Http/Resources/EntryResource.php
Normal file
32
app/Http/Resources/EntryResource.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class EntryResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => $this->title,
|
||||||
|
'slug' => $this->slug,
|
||||||
|
'description' => $this->description,
|
||||||
|
'is_published' => $this->is_published,
|
||||||
|
'is_featured' => $this->is_featured,
|
||||||
|
'published_at' => $this->published_at,
|
||||||
|
'content' => $this->content,
|
||||||
|
'category' => $this->category->name ?? null,
|
||||||
|
'featured_image_url' => $this->getFirstMediaUrl('featured-image') ?: null,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Http/Resources/TextWidgetResource.php
Normal file
27
app/Http/Resources/TextWidgetResource.php
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class TextWidgetResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => $this->title,
|
||||||
|
'description' => $this->description,
|
||||||
|
'content' => $this->content,
|
||||||
|
'category' => $this->category->name ?? null,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/Models/Category.php
Normal file
10
app/Models/Category.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Category extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['name'];
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichConten
|
||||||
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
@ -16,11 +17,9 @@ use Spatie\Tags\HasTags;
|
||||||
Entry model with rich content and media library integration
|
Entry model with rich content and media library integration
|
||||||
This is the main article / blog rich content model
|
This is the main article / blog rich content model
|
||||||
*/
|
*/
|
||||||
class Entry extends Model implements HasRichContent, HasMedia
|
class Entry extends Model implements HasMedia, HasRichContent
|
||||||
|
|
||||||
{
|
{
|
||||||
|
use HasFactory, HasTags, InteractsWithMedia, InteractsWithRichContent;
|
||||||
use InteractsWithMedia, InteractsWithRichContent, HasFactory, HasTags;
|
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'title',
|
'title',
|
||||||
|
|
@ -30,9 +29,9 @@ class Entry extends Model implements HasRichContent, HasMedia
|
||||||
'is_featured',
|
'is_featured',
|
||||||
'published_at',
|
'published_at',
|
||||||
'content',
|
'content',
|
||||||
|
'category_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up rich content configuration for media library integration
|
* Set up rich content configuration for media library integration
|
||||||
*/
|
*/
|
||||||
|
|
@ -56,4 +55,9 @@ class Entry extends Model implements HasRichContent, HasMedia
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class TextWidget extends Model
|
class TextWidget extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -13,5 +14,11 @@ class TextWidget extends Model
|
||||||
'title',
|
'title',
|
||||||
'description',
|
'description',
|
||||||
'content',
|
'content',
|
||||||
|
'category_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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('categories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('categories');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?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::table('text_widgets', function (Blueprint $table) {
|
||||||
|
$table->foreignId('category_id')->nullable()->constrained('categories');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('text_widgets', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['category_id']);
|
||||||
|
$table->dropColumn('category_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?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::table('entries', function (Blueprint $table) {
|
||||||
|
$table->foreignId('category_id')->nullable()->constrained('categories');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('entries', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['category_id']);
|
||||||
|
$table->dropColumn('category_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Config;
|
use Illuminate\Support\Facades\Config;
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ test('can list entries', function () {
|
||||||
$response = $this->actingAs($user)->getJson('/api/entries');
|
$response = $this->actingAs($user)->getJson('/api/entries');
|
||||||
|
|
||||||
$response->assertOk()
|
$response->assertOk()
|
||||||
->assertJsonCount(3);
|
->assertJsonCount(3, 'data');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can create an entry', function () {
|
test('can create an entry', function () {
|
||||||
|
|
@ -30,7 +30,7 @@ test('can create an entry', function () {
|
||||||
$response = $this->postJson('/api/entries', $data);
|
$response = $this->postJson('/api/entries', $data);
|
||||||
|
|
||||||
$response->assertCreated()
|
$response->assertCreated()
|
||||||
->assertJsonFragment($data);
|
->assertJsonFragment($data);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can show an entry', function () {
|
test('can show an entry', function () {
|
||||||
|
|
@ -40,7 +40,7 @@ test('can show an entry', function () {
|
||||||
$response = $this->actingAs($user)->getJson("/api/entries/{$entry->id}");
|
$response = $this->actingAs($user)->getJson("/api/entries/{$entry->id}");
|
||||||
|
|
||||||
$response->assertOk()
|
$response->assertOk()
|
||||||
->assertJsonFragment(['id' => $entry->id]);
|
->assertJsonFragment(['id' => $entry->id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can update an entry', function () {
|
test('can update an entry', function () {
|
||||||
|
|
@ -51,7 +51,7 @@ test('can update an entry', function () {
|
||||||
$response = $this->actingAs($user)->putJson("/api/entries/{$entry->id}", $data);
|
$response = $this->actingAs($user)->putJson("/api/entries/{$entry->id}", $data);
|
||||||
|
|
||||||
$response->assertOk()
|
$response->assertOk()
|
||||||
->assertJsonFragment($data);
|
->assertJsonFragment($data);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can delete an entry', function () {
|
test('can delete an entry', function () {
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ it('has correct fillable attributes', function () {
|
||||||
'is_featured',
|
'is_featured',
|
||||||
'published_at',
|
'published_at',
|
||||||
'content',
|
'content',
|
||||||
|
'category_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
$entry = new Entry();
|
$entry = new Entry;
|
||||||
|
|
||||||
expect($entry->getFillable())->toEqual($expected);
|
expect($entry->getFillable())->toEqual($expected);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ it('has correct fillable attributes', function () {
|
||||||
'title',
|
'title',
|
||||||
'description',
|
'description',
|
||||||
'content',
|
'content',
|
||||||
|
'category_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
$entry = new TextWidget;
|
$entry = new TextWidget;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue