libra/resources/views/livewire/admin/posts/edit.blade.php

333 lines
12 KiB
PHP

<?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 bool $showDeleteModal = 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;
}
public function delete(): void
{
$this->showDeleteModal = true;
}
public function confirmDelete(): void
{
AdminLog::create([
'admin_id' => auth()->id(),
'action' => 'delete',
'target_type' => 'post',
'target_id' => $this->post->id,
'old_values' => $this->post->toArray(),
'ip_address' => request()->ip(),
'created_at' => now(),
]);
$this->post->delete();
session()->flash('success', __('posts.post_deleted'));
$this->redirect(route('admin.posts.index'), navigate: true);
}
public function cancelDelete(): void
{
$this->showDeleteModal = 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 class="required">{{ __('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 class="required">{{ __('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 class="required">{{ __('posts.title') }} ({{ __('posts.english') }})</flux:label>
<flux:input wire:model="title_en" required />
<flux:error name="title_en" />
</flux:field>
<flux:field>
<flux:label class="required">{{ __('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-between gap-4 border-t border-zinc-200 pt-6 dark:border-zinc-700">
<div>
<flux:button variant="danger" type="button" wire:click="delete">
{{ __('posts.delete_post') }}
</flux:button>
</div>
<div class="flex items-center gap-4">
<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>
</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>
{{-- Delete Confirmation Modal --}}
<flux:modal wire:model="showDeleteModal">
<div class="space-y-4">
<flux:heading size="lg">{{ __('posts.delete_post') }}</flux:heading>
<flux:callout variant="danger">
{{ __('posts.delete_post_warning') }}
</flux:callout>
<p class="text-zinc-700 dark:text-zinc-300">
{{ __('posts.deleting_post', ['title' => $post->getTitle()]) }}
</p>
<div class="flex gap-3 justify-end pt-4">
<flux:button wire:click="cancelDelete">
{{ __('common.cancel') }}
</flux:button>
<flux:button variant="danger" wire:click="confirmDelete">
{{ __('posts.delete_permanently') }}
</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