11 KiB
11 KiB
Admin Package
The Admin package provides a complete admin panel with Livewire modals, form components, global search, and an extensible menu system.
Installation
composer require host-uk/core-admin
Features
Admin Menu System
Extensible navigation menu with automatic discovery:
<?php
namespace Mod\Blog;
use Core\Front\Admin\Contracts\AdminMenuProvider;
use Core\Front\Admin\Support\MenuItemBuilder;
class BlogMenuProvider implements AdminMenuProvider
{
public function register(): array
{
return [
MenuItemBuilder::make('Blog')
->icon('newspaper')
->priority(30)
->children([
MenuItemBuilder::make('Posts')
->route('admin.blog.posts.index')
->icon('document-text'),
MenuItemBuilder::make('Categories')
->route('admin.blog.categories.index')
->icon('folder'),
])
->build(),
];
}
}
Register in your module's Boot.php:
public function onAdmin(AdminPanelBooting $event): void
{
$event->menu(new BlogMenuProvider());
}
Learn more about Admin Menus →
Livewire Modals
Full-page modal system for admin interfaces:
<?php
namespace Mod\Blog\View\Modal\Admin;
use Livewire\Component;
class PostEditor extends Component
{
public ?Post $post = null;
public $title;
public $content;
public function mount(?Post $post = null): void
{
$this->post = $post;
$this->title = $post?->title;
$this->content = $post?->content;
}
public function save(): void
{
$validated = $this->validate([
'title' => 'required|max:255',
'content' => 'required',
]);
if ($this->post) {
$this->post->update($validated);
} else {
Post::create($validated);
}
$this->dispatch('post-saved');
$this->closeModal();
}
public function render()
{
return view('blog::admin.post-editor');
}
}
Open modals from any admin page:
<x-button wire:click="$dispatch('openModal', {component: 'blog.post-editor'})">
New Post
</x-button>
<x-button wire:click="$dispatch('openModal', {component: 'blog.post-editor', arguments: {post: {{ $post->id }}}})">
Edit Post
</x-button>
Form Components
Pre-built form components with validation:
<x-admin::form action="{{ route('admin.posts.store') }}">
<x-admin::form-group
label="Title"
name="title"
required
>
<x-admin::input
name="title"
:value="old('title', $post->title)"
placeholder="Enter post title"
/>
</x-admin::form-group>
<x-admin::form-group
label="Content"
name="content"
required
>
<x-admin::textarea
name="content"
:value="old('content', $post->content)"
rows="10"
/>
</x-admin::form-group>
<x-admin::form-group
label="Category"
name="category_id"
>
<x-admin::select
name="category_id"
:options="$categories"
:selected="old('category_id', $post->category_id)"
/>
</x-admin::form-group>
<x-admin::form-group
label="Published"
name="is_published"
>
<x-admin::toggle
name="is_published"
:checked="old('is_published', $post->is_published)"
/>
</x-admin::form-group>
<div class="flex justify-end space-x-2">
<x-admin::button type="submit" variant="primary">
Save Post
</x-admin::button>
<x-admin::button type="button" variant="secondary" onclick="history.back()">
Cancel
</x-admin::button>
</div>
</x-admin::form>
Global Search
Search across all admin content:
<?php
namespace Mod\Blog\Search;
use Core\Admin\Search\Contracts\SearchProvider;
use Core\Admin\Search\SearchResult;
use Mod\Blog\Models\Post;
class PostSearchProvider implements SearchProvider
{
public function search(string $query): array
{
return Post::where('title', 'like', "%{$query}%")
->orWhere('content', 'like', "%{$query}%")
->limit(5)
->get()
->map(fn ($post) => new SearchResult(
title: $post->title,
description: $post->excerpt,
url: route('admin.blog.posts.edit', $post),
icon: 'document-text',
type: 'Post',
))
->toArray();
}
public function getSearchableTypes(): array
{
return ['posts'];
}
}
Register provider:
// config/core-admin.php
'search' => [
'providers' => [
\Mod\Blog\Search\PostSearchProvider::class,
],
],
Dashboard Widgets
Add widgets to the admin dashboard:
<?php
namespace Mod\Blog\Widgets;
use Livewire\Component;
class PostStatsWidget extends Component
{
public function render()
{
return view('blog::admin.widgets.post-stats', [
'totalPosts' => Post::count(),
'publishedPosts' => Post::published()->count(),
'draftPosts' => Post::draft()->count(),
]);
}
}
Register widget:
public function onAdmin(AdminPanelBooting $event): void
{
$event->widget(new PostStatsWidget(), priority: 10);
}
Settings Pages
Add custom settings pages:
<?php
namespace Mod\Blog\Settings;
use Livewire\Component;
class BlogSettings extends Component
{
public $postsPerPage;
public $enableComments;
public function mount(): void
{
$this->postsPerPage = config('blog.posts_per_page', 10);
$this->enableComments = config('blog.comments_enabled', true);
}
public function save(): void
{
ConfigService::set('blog.posts_per_page', $this->postsPerPage);
ConfigService::set('blog.comments_enabled', $this->enableComments);
$this->dispatch('settings-saved');
}
public function render()
{
return view('blog::admin.settings');
}
}
Register settings page:
public function onAdmin(AdminPanelBooting $event): void
{
$event->settings('blog', BlogSettings::class);
}
Components Reference
Input
<x-admin::input
name="title"
type="text"
:value="$value"
placeholder="Enter title"
required
disabled
readonly
/>
Textarea
<x-admin::textarea
name="content"
:value="$value"
rows="10"
placeholder="Enter content"
/>
Select
<x-admin::select
name="category"
:options="[1 => 'Tech', 2 => 'Design']"
:selected="$selectedId"
placeholder="Select category"
/>
Checkbox
<x-admin::checkbox
name="terms"
:checked="$isChecked"
label="I agree to terms"
/>
Toggle
<x-admin::toggle
name="is_active"
:checked="$isActive"
label="Active"
/>
Button
<x-admin::button
type="submit"
variant="primary|secondary|danger"
size="sm|md|lg"
icon="save"
disabled
loading
>
Save Changes
</x-admin::button>
Form Group
<x-admin::form-group
label="Email"
name="email"
help="We'll never share your email"
error="$errors->first('email')"
required
>
<x-admin::input name="email" type="email" />
</x-admin::form-group>
Layouts
Admin App Layout
<x-admin::layout>
<x-slot:header>
<h1>Page Title</h1>
</x-slot>
{{-- Main content --}}
<div class="container mx-auto">
Content here
</div>
</x-admin::layout>
HLCRF Layout
<x-hlcrf::layout>
<x-hlcrf::header>
Page Header with Actions
</x-hlcrf::header>
<x-hlcrf::left>
Sidebar Navigation
</x-hlcrf::left>
<x-hlcrf::content>
Main Content Area
</x-hlcrf::content>
<x-hlcrf::right>
Contextual Help & Widgets
</x-hlcrf::right>
</x-hlcrf::layout>
Configuration
// config/core-admin.php
return [
'menu' => [
'cache_enabled' => true,
'cache_ttl' => 3600,
'show_icons' => true,
],
'search' => [
'enabled' => true,
'providers' => [
// Register search providers
],
'max_results' => 10,
],
'livewire' => [
'modal_max_width' => '7xl',
'modal_close_on_escape' => true,
],
'form' => [
'validation_real_time' => true,
'show_required_indicator' => true,
],
];
Styling
Admin package uses Tailwind CSS. Customize theme:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
admin: {
primary: '#3b82f6',
secondary: '#64748b',
success: '#22c55e',
danger: '#ef4444',
},
},
},
},
};
JavaScript
Admin package includes Alpine.js for interactivity:
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">
Content
</div>
</div>
Testing
Feature Tests
public function test_can_access_admin_dashboard(): void
{
$user = User::factory()->admin()->create();
$response = $this->actingAs($user)
->get('/admin');
$response->assertStatus(200);
}
public function test_admin_menu_displays_blog_items(): void
{
$user = User::factory()->admin()->create();
$response = $this->actingAs($user)
->get('/admin');
$response->assertSee('Blog');
$response->assertSee('Posts');
$response->assertSee('Categories');
}
Livewire Component Tests
public function test_can_create_post_via_modal(): void
{
Livewire::actingAs($admin)
->test(PostEditor::class)
->set('title', 'Test Post')
->set('content', 'Test content')
->call('save')
->assertDispatched('post-saved');
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
]);
}
Best Practices
1. Use Livewire Modals for CRUD
// ✅ Good - modal UX
<x-button wire:click="$dispatch('openModal', {component: 'post-editor'})">
New Post
</x-button>
// ❌ Bad - full page redirect
<a href="{{ route('admin.posts.create') }}">New Post</a>
2. Organize Menu Items by Domain
MenuItemBuilder::make('Content')
->children([
MenuItemBuilder::make('Posts')->route('admin.posts.index'),
MenuItemBuilder::make('Pages')->route('admin.pages.index'),
]);
3. Use Form Components
{{-- ✅ Good - consistent styling --}}
<x-admin::form-group label="Title" name="title">
<x-admin::input name="title" />
</x-admin::form-group>
{{-- ❌ Bad - custom HTML --}}
<div class="mb-4">
<label>Title</label>
<input type="text" name="title">
</div>
Changelog
See CHANGELOG.md
License
EUPL-1.2