complete story 5.1 with qa tests
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\Post;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
public ?Post $post = null;
|
||||
|
||||
public string $title_ar = '';
|
||||
public string $title_en = '';
|
||||
public string $body_ar = '';
|
||||
public string $body_en = '';
|
||||
public string $status = 'draft';
|
||||
|
||||
public bool $showPreview = false;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title_ar' => ['required', 'string', 'max:255'],
|
||||
'title_en' => ['required', 'string', 'max:255'],
|
||||
'body_ar' => ['required', 'string'],
|
||||
'body_en' => ['required', 'string'],
|
||||
'status' => ['required', 'in:draft,published'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title_ar.required' => __('posts.title_ar_required'),
|
||||
'title_en.required' => __('posts.title_en_required'),
|
||||
'body_ar.required' => __('posts.body_ar_required'),
|
||||
'body_en.required' => __('posts.body_en_required'),
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$validated = $this->validate();
|
||||
|
||||
$postData = [
|
||||
'title' => [
|
||||
'ar' => $validated['title_ar'],
|
||||
'en' => $validated['title_en'],
|
||||
],
|
||||
'body' => [
|
||||
'ar' => clean($validated['body_ar']),
|
||||
'en' => clean($validated['body_en']),
|
||||
],
|
||||
'status' => $validated['status'],
|
||||
'published_at' => $validated['status'] === 'published' ? now() : null,
|
||||
];
|
||||
|
||||
if ($this->post) {
|
||||
$oldValues = $this->post->toArray();
|
||||
$this->post->update($postData);
|
||||
$action = 'update';
|
||||
$newValues = $this->post->fresh()->toArray();
|
||||
} else {
|
||||
$this->post = Post::create($postData);
|
||||
$action = 'create';
|
||||
$oldValues = null;
|
||||
$newValues = $this->post->toArray();
|
||||
}
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => $action,
|
||||
'target_type' => 'post',
|
||||
'target_id' => $this->post->id,
|
||||
'old_values' => $oldValues,
|
||||
'new_values' => $newValues,
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
session()->flash('success', __('posts.post_saved'));
|
||||
|
||||
$this->redirect(route('admin.posts.edit', $this->post), navigate: true);
|
||||
}
|
||||
|
||||
public function saveDraft(): void
|
||||
{
|
||||
$this->status = 'draft';
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function publish(): void
|
||||
{
|
||||
$this->status = 'published';
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function autoSave(): void
|
||||
{
|
||||
if ($this->post && $this->status === 'draft') {
|
||||
$this->post->update([
|
||||
'title' => [
|
||||
'ar' => $this->title_ar,
|
||||
'en' => $this->title_en,
|
||||
],
|
||||
'body' => [
|
||||
'ar' => clean($this->body_ar),
|
||||
'en' => clean($this->body_en),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function preview(): void
|
||||
{
|
||||
$this->showPreview = true;
|
||||
}
|
||||
|
||||
public function closePreview(): void
|
||||
{
|
||||
$this->showPreview = false;
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div wire:poll.60s="autoSave">
|
||||
<div class="mb-6">
|
||||
<flux:button variant="ghost" :href="route('admin.posts.index')" wire:navigate icon="arrow-left">
|
||||
{{ __('posts.back_to_posts') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<flux:heading size="xl">{{ __('posts.create_post') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<flux:callout variant="success" class="mb-6">
|
||||
{{ session('success') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<form wire:submit="save" class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Arabic Fields -->
|
||||
<div class="space-y-4">
|
||||
<flux:heading size="sm">{{ __('posts.arabic_content') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.title') }} ({{ __('posts.arabic') }}) *</flux:label>
|
||||
<flux:input wire:model="title_ar" dir="rtl" required />
|
||||
<flux:error name="title_ar" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.body') }} ({{ __('posts.arabic') }}) *</flux:label>
|
||||
<div wire:ignore>
|
||||
<input id="body_ar" type="hidden" wire:model="body_ar">
|
||||
<trix-editor
|
||||
input="body_ar"
|
||||
dir="rtl"
|
||||
class="trix-content prose prose-sm max-w-none bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700 min-h-[200px]"
|
||||
x-data
|
||||
x-on:trix-change="$wire.set('body_ar', $event.target.value)"
|
||||
></trix-editor>
|
||||
</div>
|
||||
<flux:error name="body_ar" />
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<!-- English Fields -->
|
||||
<div class="space-y-4">
|
||||
<flux:heading size="sm">{{ __('posts.english_content') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.title') }} ({{ __('posts.english') }}) *</flux:label>
|
||||
<flux:input wire:model="title_en" required />
|
||||
<flux:error name="title_en" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.body') }} ({{ __('posts.english') }}) *</flux:label>
|
||||
<div wire:ignore>
|
||||
<input id="body_en" type="hidden" wire:model="body_en">
|
||||
<trix-editor
|
||||
input="body_en"
|
||||
class="trix-content prose prose-sm max-w-none bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700 min-h-[200px]"
|
||||
x-data
|
||||
x-on:trix-change="$wire.set('body_en', $event.target.value)"
|
||||
></trix-editor>
|
||||
</div>
|
||||
<flux:error name="body_en" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-4 border-t border-zinc-200 pt-6 dark:border-zinc-700">
|
||||
<flux:button variant="ghost" :href="route('admin.posts.index')" wire:navigate>
|
||||
{{ __('common.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" wire:click="preview">
|
||||
{{ __('posts.preview') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" wire:click="saveDraft">
|
||||
{{ __('posts.save_draft') }}
|
||||
</flux:button>
|
||||
<flux:button variant="primary" type="button" wire:click="publish">
|
||||
{{ __('posts.publish') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Preview Modal --}}
|
||||
<flux:modal wire:model="showPreview" class="max-w-4xl">
|
||||
<div class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('posts.preview') }}</flux:heading>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="border-b border-zinc-200 dark:border-zinc-700 pb-4">
|
||||
<h3 class="font-semibold text-lg mb-2 text-zinc-600 dark:text-zinc-400">{{ __('posts.arabic_content') }}</h3>
|
||||
<h2 class="text-xl font-bold text-zinc-900 dark:text-zinc-100" dir="rtl">{{ $title_ar }}</h2>
|
||||
<div class="prose prose-sm dark:prose-invert mt-2 max-w-none" dir="rtl">{!! clean($body_ar) !!}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-2 text-zinc-600 dark:text-zinc-400">{{ __('posts.english_content') }}</h3>
|
||||
<h2 class="text-xl font-bold text-zinc-900 dark:text-zinc-100">{{ $title_en }}</h2>
|
||||
<div class="prose prose-sm dark:prose-invert mt-2 max-w-none">{!! clean($body_en) !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<flux:button wire:click="closePreview">{{ __('common.close') }}</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="https://unpkg.com/trix@2.0.0/dist/trix.css">
|
||||
<style>
|
||||
trix-toolbar [data-trix-button-group="file-tools"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/trix@2.0.0/dist/trix.umd.min.js"></script>
|
||||
@endpush
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\Post;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
public Post $post;
|
||||
|
||||
public string $title_ar = '';
|
||||
public string $title_en = '';
|
||||
public string $body_ar = '';
|
||||
public string $body_en = '';
|
||||
public string $status = 'draft';
|
||||
|
||||
public bool $showPreview = false;
|
||||
|
||||
public function mount(Post $post): void
|
||||
{
|
||||
$this->post = $post;
|
||||
$this->title_ar = $post->title['ar'] ?? '';
|
||||
$this->title_en = $post->title['en'] ?? '';
|
||||
$this->body_ar = $post->body['ar'] ?? '';
|
||||
$this->body_en = $post->body['en'] ?? '';
|
||||
$this->status = $post->status->value;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title_ar' => ['required', 'string', 'max:255'],
|
||||
'title_en' => ['required', 'string', 'max:255'],
|
||||
'body_ar' => ['required', 'string'],
|
||||
'body_en' => ['required', 'string'],
|
||||
'status' => ['required', 'in:draft,published'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title_ar.required' => __('posts.title_ar_required'),
|
||||
'title_en.required' => __('posts.title_en_required'),
|
||||
'body_ar.required' => __('posts.body_ar_required'),
|
||||
'body_en.required' => __('posts.body_en_required'),
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$validated = $this->validate();
|
||||
|
||||
$oldValues = $this->post->toArray();
|
||||
|
||||
$this->post->update([
|
||||
'title' => [
|
||||
'ar' => $validated['title_ar'],
|
||||
'en' => $validated['title_en'],
|
||||
],
|
||||
'body' => [
|
||||
'ar' => clean($validated['body_ar']),
|
||||
'en' => clean($validated['body_en']),
|
||||
],
|
||||
'status' => $validated['status'],
|
||||
'published_at' => $validated['status'] === 'published' && ! $this->post->published_at
|
||||
? now()
|
||||
: $this->post->published_at,
|
||||
]);
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'update',
|
||||
'target_type' => 'post',
|
||||
'target_id' => $this->post->id,
|
||||
'old_values' => $oldValues,
|
||||
'new_values' => $this->post->fresh()->toArray(),
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
session()->flash('success', __('posts.post_saved'));
|
||||
}
|
||||
|
||||
public function saveDraft(): void
|
||||
{
|
||||
$this->status = 'draft';
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function publish(): void
|
||||
{
|
||||
$this->status = 'published';
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function autoSave(): void
|
||||
{
|
||||
if ($this->status === 'draft') {
|
||||
$this->post->update([
|
||||
'title' => [
|
||||
'ar' => $this->title_ar,
|
||||
'en' => $this->title_en,
|
||||
],
|
||||
'body' => [
|
||||
'ar' => clean($this->body_ar),
|
||||
'en' => clean($this->body_en),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function preview(): void
|
||||
{
|
||||
$this->showPreview = true;
|
||||
}
|
||||
|
||||
public function closePreview(): void
|
||||
{
|
||||
$this->showPreview = false;
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div wire:poll.60s="autoSave">
|
||||
<div class="mb-6">
|
||||
<flux:button variant="ghost" :href="route('admin.posts.index')" wire:navigate icon="arrow-left">
|
||||
{{ __('posts.back_to_posts') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('posts.edit_post') }}</flux:heading>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
{{ __('posts.last_updated') }}: {{ $post->updated_at->diffForHumans() }}
|
||||
@if($status === 'draft')
|
||||
<span class="text-amber-600 dark:text-amber-400">({{ __('posts.auto_save_enabled') }})</span>
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<flux:badge :color="$status === 'published' ? 'green' : 'amber'">
|
||||
{{ __('enums.post_status.' . $status) }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<flux:callout variant="success" class="mb-6">
|
||||
{{ session('success') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<form wire:submit="save" class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Arabic Fields -->
|
||||
<div class="space-y-4">
|
||||
<flux:heading size="sm">{{ __('posts.arabic_content') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.title') }} ({{ __('posts.arabic') }}) *</flux:label>
|
||||
<flux:input wire:model="title_ar" dir="rtl" required />
|
||||
<flux:error name="title_ar" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.body') }} ({{ __('posts.arabic') }}) *</flux:label>
|
||||
<div wire:ignore>
|
||||
<input id="body_ar" type="hidden" wire:model="body_ar" value="{{ $body_ar }}">
|
||||
<trix-editor
|
||||
input="body_ar"
|
||||
dir="rtl"
|
||||
class="trix-content prose prose-sm max-w-none bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700 min-h-[200px]"
|
||||
x-data
|
||||
x-on:trix-change="$wire.set('body_ar', $event.target.value)"
|
||||
></trix-editor>
|
||||
</div>
|
||||
<flux:error name="body_ar" />
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<!-- English Fields -->
|
||||
<div class="space-y-4">
|
||||
<flux:heading size="sm">{{ __('posts.english_content') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.title') }} ({{ __('posts.english') }}) *</flux:label>
|
||||
<flux:input wire:model="title_en" required />
|
||||
<flux:error name="title_en" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('posts.body') }} ({{ __('posts.english') }}) *</flux:label>
|
||||
<div wire:ignore>
|
||||
<input id="body_en" type="hidden" wire:model="body_en" value="{{ $body_en }}">
|
||||
<trix-editor
|
||||
input="body_en"
|
||||
class="trix-content prose prose-sm max-w-none bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700 min-h-[200px]"
|
||||
x-data
|
||||
x-on:trix-change="$wire.set('body_en', $event.target.value)"
|
||||
></trix-editor>
|
||||
</div>
|
||||
<flux:error name="body_en" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-4 border-t border-zinc-200 pt-6 dark:border-zinc-700">
|
||||
<flux:button variant="ghost" :href="route('admin.posts.index')" wire:navigate>
|
||||
{{ __('common.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" wire:click="preview">
|
||||
{{ __('posts.preview') }}
|
||||
</flux:button>
|
||||
@if($status === 'published')
|
||||
<flux:button type="button" wire:click="saveDraft">
|
||||
{{ __('posts.unpublish') }}
|
||||
</flux:button>
|
||||
<flux:button variant="primary" type="button" wire:click="publish">
|
||||
{{ __('posts.save_changes') }}
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:button type="button" wire:click="saveDraft">
|
||||
{{ __('posts.save_draft') }}
|
||||
</flux:button>
|
||||
<flux:button variant="primary" type="button" wire:click="publish">
|
||||
{{ __('posts.publish') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Preview Modal --}}
|
||||
<flux:modal wire:model="showPreview" class="max-w-4xl">
|
||||
<div class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('posts.preview') }}</flux:heading>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="border-b border-zinc-200 dark:border-zinc-700 pb-4">
|
||||
<h3 class="font-semibold text-lg mb-2 text-zinc-600 dark:text-zinc-400">{{ __('posts.arabic_content') }}</h3>
|
||||
<h2 class="text-xl font-bold text-zinc-900 dark:text-zinc-100" dir="rtl">{{ $title_ar }}</h2>
|
||||
<div class="prose prose-sm dark:prose-invert mt-2 max-w-none" dir="rtl">{!! clean($body_ar) !!}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-2 text-zinc-600 dark:text-zinc-400">{{ __('posts.english_content') }}</h3>
|
||||
<h2 class="text-xl font-bold text-zinc-900 dark:text-zinc-100">{{ $title_en }}</h2>
|
||||
<div class="prose prose-sm dark:prose-invert mt-2 max-w-none">{!! clean($body_en) !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<flux:button wire:click="closePreview">{{ __('common.close') }}</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="https://unpkg.com/trix@2.0.0/dist/trix.css">
|
||||
<style>
|
||||
trix-toolbar [data-trix-button-group="file-tools"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/trix@2.0.0/dist/trix.umd.min.js"></script>
|
||||
@endpush
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Models\Post;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public string $search = '';
|
||||
public string $statusFilter = '';
|
||||
public string $sortBy = 'updated_at';
|
||||
public string $sortDir = 'desc';
|
||||
public int $perPage = 15;
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedStatusFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedPerPage(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function sort(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDir = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->statusFilter = '';
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
|
||||
return [
|
||||
'posts' => Post::query()
|
||||
->when($this->search, fn ($q) => $q->where(function ($q) use ($locale) {
|
||||
$q->whereRaw("JSON_EXTRACT(title, '$.\"{$locale}\"') LIKE ?", ["%{$this->search}%"])
|
||||
->orWhereRaw("JSON_EXTRACT(title, '$.\"ar\"') LIKE ?", ["%{$this->search}%"])
|
||||
->orWhereRaw("JSON_EXTRACT(title, '$.\"en\"') LIKE ?", ["%{$this->search}%"]);
|
||||
}))
|
||||
->when($this->statusFilter, fn ($q) => $q->where('status', $this->statusFilter))
|
||||
->orderBy($this->sortBy, $this->sortDir)
|
||||
->paginate($this->perPage),
|
||||
'statuses' => PostStatus::cases(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('posts.posts') }}</flux:heading>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{{ __('posts.posts_description') }}</p>
|
||||
</div>
|
||||
<flux:button href="{{ route('admin.posts.create') }}" variant="primary" icon="plus" wire:navigate>
|
||||
{{ __('posts.create_post') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<flux:callout variant="success" class="mb-6">
|
||||
{{ session('success') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<flux:callout variant="danger" class="mb-6">
|
||||
{{ session('error') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<flux:field>
|
||||
<flux:input
|
||||
wire:model.live.debounce.300ms="search"
|
||||
placeholder="{{ __('posts.search_placeholder') }}"
|
||||
icon="magnifying-glass"
|
||||
/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:select wire:model.live="statusFilter">
|
||||
<option value="">{{ __('admin.all_statuses') }}</option>
|
||||
@foreach($statuses as $status)
|
||||
<option value="{{ $status->value }}">{{ __('enums.post_status.' . $status->value) }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:select wire:model.live="perPage">
|
||||
<option value="15">15 {{ __('admin.per_page') }}</option>
|
||||
<option value="25">25 {{ __('admin.per_page') }}</option>
|
||||
<option value="50">50 {{ __('admin.per_page') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
@if($search || $statusFilter)
|
||||
<div class="flex items-end">
|
||||
<flux:button wire:click="clearFilters" variant="ghost">
|
||||
{{ __('common.clear') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort Headers -->
|
||||
<div class="hidden lg:flex bg-zinc-100 dark:bg-zinc-700 rounded-t-lg px-4 py-2 text-sm font-medium text-zinc-600 dark:text-zinc-300 gap-4 mb-0">
|
||||
<button wire:click="sort('title')" class="flex items-center gap-1 flex-1 hover:text-zinc-900 dark:hover:text-white">
|
||||
{{ __('posts.title') }}
|
||||
@if($sortBy === 'title')
|
||||
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
<span class="w-24">{{ __('admin.current_status') }}</span>
|
||||
<button wire:click="sort('updated_at')" class="flex items-center gap-1 w-32 hover:text-zinc-900 dark:hover:text-white">
|
||||
{{ __('posts.last_updated') }}
|
||||
@if($sortBy === 'updated_at')
|
||||
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
<button wire:click="sort('created_at')" class="flex items-center gap-1 w-32 hover:text-zinc-900 dark:hover:text-white">
|
||||
{{ __('posts.created') }}
|
||||
@if($sortBy === 'created_at')
|
||||
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
<span class="w-32">{{ __('common.actions') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Posts List -->
|
||||
<div class="space-y-0">
|
||||
@forelse($posts as $post)
|
||||
<div wire:key="post-{{ $post->id }}" class="bg-white dark:bg-zinc-800 p-4 border border-zinc-200 dark:border-zinc-700 {{ $loop->first ? 'rounded-t-lg lg:rounded-t-none' : '' }} {{ $loop->last ? 'rounded-b-lg' : '' }} {{ !$loop->first ? 'border-t-0' : '' }}">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center gap-4">
|
||||
<!-- Title -->
|
||||
<div class="flex-1">
|
||||
<a href="{{ route('admin.posts.edit', $post) }}" class="font-semibold text-zinc-900 dark:text-zinc-100 hover:text-blue-600 dark:hover:text-blue-400" wire:navigate>
|
||||
{{ $post->getTitle() }}
|
||||
</a>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400 mt-1 line-clamp-2">
|
||||
{{ $post->getExcerpt() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="lg:w-24">
|
||||
<flux:badge :color="$post->status === \App\Enums\PostStatus::Published ? 'green' : 'amber'" size="sm">
|
||||
{{ __('enums.post_status.' . $post->status->value) }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
|
||||
<!-- Last Updated -->
|
||||
<div class="lg:w-32">
|
||||
<div class="text-sm text-zinc-900 dark:text-zinc-100">
|
||||
{{ $post->updated_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Created -->
|
||||
<div class="lg:w-32">
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $post->created_at->format('Y-m-d') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="lg:w-32 flex gap-2">
|
||||
<flux:button
|
||||
href="{{ route('admin.posts.edit', $post) }}"
|
||||
variant="filled"
|
||||
size="sm"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('common.edit') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400 bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:icon name="document-text" class="w-12 h-12 mx-auto mb-4" />
|
||||
<p>{{ __('posts.no_posts') }}</p>
|
||||
<div class="mt-4">
|
||||
<flux:button href="{{ route('admin.posts.create') }}" variant="primary" icon="plus" wire:navigate>
|
||||
{{ __('posts.create_post') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user