This commit is contained in:
Mark Gregory
2026-07-24 10:30:36 +01:00
parent 10e9c25d6c
commit 395e4dc545
94 changed files with 619 additions and 160 deletions
+2
View File
@@ -0,0 +1,2 @@
<?php
return ['name' => 'Categories'];
@@ -0,0 +1,8 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void { Schema::create('categories', function (Blueprint $table): void { $table->id(); $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete(); $table->string('name'); $table->string('slug')->unique(); $table->text('description')->nullable(); $table->string('type')->default('content')->index(); $table->unsignedInteger('sort_order')->default(0); $table->boolean('is_active')->default(true); $table->timestamps(); }); }
public function down(): void { Schema::dropIfExists('categories'); }
};
@@ -0,0 +1,17 @@
<?php
namespace Modules\Categories\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Modules\Categories\Models\Category;
final class CategoryController extends Controller
{
public function index(): View { return view('categories::admin.index', ['records'=>Category::with('parent')->orderBy('type')->orderBy('sort_order')->paginate(30)]); }
public function create(): View { return view('categories::admin.form', ['parents'=>Category::orderBy('name')->get()]); }
public function store(Request $request): RedirectResponse { Category::create($this->validated($request)); return redirect()->route('administration.categories.index')->with('status','Category created.'); }
public function edit(Category $category): View { return view('categories::admin.form', ['record'=>$category,'parents'=>Category::whereKeyNot($category->getKey())->orderBy('name')->get()]); }
public function update(Request $request, Category $category): RedirectResponse { $category->update($this->validated($request)); return redirect()->route('administration.categories.index')->with('status','Category updated.'); }
public function destroy(Category $category): RedirectResponse { $category->delete(); return back()->with('status','Category deleted.'); }
private function validated(Request $request): array { $data=$request->validate(['parent_id'=>['nullable','exists:categories,id'],'name'=>['required','string','max:255'],'slug'=>['required','string','max:255'],'description'=>['nullable','string'],'type'=>['required','string','max:50'],'sort_order'=>['nullable','integer','min:0'],'is_active'=>['sometimes','boolean']]); $data['is_active']=$request->boolean('is_active'); return $data; }
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Modules\Categories\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Category extends Model
{
protected $fillable = ['parent_id','name','slug','description','type','sort_order','is_active'];
protected function casts(): array { return ['is_active'=>'boolean','sort_order'=>'integer']; }
public function parent(): BelongsTo { return $this->belongsTo(self::class, 'parent_id'); }
public function children(): HasMany { return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); }
}
@@ -0,0 +1,16 @@
<?php
namespace Modules\Categories\Providers;
use Illuminate\Support\ServiceProvider;
final class CategoriesServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path('Categories', 'Config/config.php'), 'categories');
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path('Categories', 'Database/Migrations'));
$this->loadViewsFrom(module_path('Categories', 'Resources/views'), 'categories');
}
}
@@ -0,0 +1,12 @@
<?php
namespace Modules\Categories\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
Route::middleware('web')->group(module_path('Categories', 'Routes/web.php'));
}
}
@@ -0,0 +1 @@
<x-layouts.app :title="isset($record) ? 'Edit category' : 'Create category'"><form method="POST" action="{{ isset($record) ? route('administration.categories.update',$record) : route('administration.categories.store') }}" class="max-w-3xl space-y-5">@csrf @isset($record) @method('PUT') @endisset <flux:input name="name" label="Name" value="{{ old('name',$record->name ?? '') }}" required/><flux:input name="slug" label="Slug" value="{{ old('slug',$record->slug ?? '') }}" required/><flux:select name="parent_id" label="Parent"><option value="">None</option>@foreach($parents as $parent)<option value="{{ $parent->id }}" @selected(old('parent_id',$record->parent_id ?? null)==$parent->id)>{{ $parent->name }}</option>@endforeach</flux:select><flux:input name="type" label="Type" value="{{ old('type',$record->type ?? 'content') }}"/><flux:textarea name="description" label="Description">{{ old('description',$record->description ?? '') }}</flux:textarea><flux:input name="sort_order" type="number" label="Sort order" value="{{ old('sort_order',$record->sort_order ?? 0) }}"/><flux:checkbox name="is_active" value="1" label="Active" :checked="old('is_active',$record->is_active ?? true)"/><flux:button type="submit" variant="primary">Save</flux:button></form></x-layouts.app>
@@ -0,0 +1 @@
<x-layouts.app :title="__('Categories')"><div class="flex items-center justify-between mb-6"><div><h1 class="text-2xl font-semibold">Categories</h1><p class="text-sm text-zinc-500">Manage publishing categories.</p></div><flux:button variant="primary" :href="route('administration.categories.create')" wire:navigate>Create</flux:button></div>@if(session('status'))<div class="mb-4 rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Name / title</th><th class="p-3 text-left">Status</th><th class="p-3 text-right">Actions</th></tr></thead><tbody>@forelse($records as $record)<tr class="border-t"><td class="p-3 font-medium">{{ $record->title ?? $record->name }}</td><td class="p-3">{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}</td><td class="p-3 text-right"><flux:button size="sm" :href="route('administration.categories.edit', $record)" wire:navigate>Edit</flux:button><form class="inline" method="POST" action="{{ route('administration.categories.destroy', $record) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="3" class="p-6 text-center text-zinc-500">No records yet.</td></tr>@endforelse</tbody></table></div><div class="mt-4">{{ $records->links() }}</div></x-layouts.app>
+4
View File
@@ -0,0 +1,4 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Categories\Http\Controllers\CategoryController;
Route::middleware(['auth','verified'])->prefix('administration/categories')->name('administration.categories.')->group(function (): void { Route::resource('/', CategoryController::class)->parameters([''=>'category'])->except('show'); });
@@ -0,0 +1,10 @@
<?php
namespace Modules\Categories\Tests\Feature;
use Tests\TestCase;
final class ModuleBootTest extends TestCase
{
public function test_module_configuration_is_available(): void
{
$this->assertSame('Categories', config('categories.name'));
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "Categories",
"alias": "categories",
"version": "1.0.0",
"description": "Categories publishing module.",
"keywords": ["publishing", "categories"],
"priority": 30,
"providers": ["Modules\\Categories\\Providers\\CategoriesServiceProvider"],
"aliases": {},
"files": [],
"requires": ["Core", "Administration"]
}