# Story 5.1: Post Creation & Editing ## Epic Reference **Epic 5:** Posts/Blog System ## User Story As an **admin**, I want **to create and edit blog posts with rich text formatting**, So that **I can publish professional legal content for website visitors**. ## Story Context ### Existing System Integration - **Integrates with:** posts table - **Technology:** Livewire Volt, TinyMCE or similar rich text editor - **Follows pattern:** Admin CRUD pattern - **Touch points:** Public posts display ## Acceptance Criteria ### Post Creation Form - [ ] Title (required, bilingual: Arabic and English) - [ ] Body content (required, bilingual) - [ ] Status (draft/published) ### Rich Text Editor - [ ] Bold, italic, underline - [ ] Headings (H2, H3) - [ ] Bullet and numbered lists - [ ] Links - [ ] Blockquotes ### Saving Features - [ ] Save as draft functionality - [ ] Preview post before publishing - [ ] Edit published posts - [ ] Auto-save draft periodically (every 60 seconds) - [ ] Immediate publishing (no scheduling) ### Timestamps - [ ] created_at recorded on creation - [ ] updated_at updated on edit ### Quality Requirements - [ ] HTML sanitization for XSS prevention - [ ] Bilingual form labels - [ ] Audit log for create/edit - [ ] Tests for CRUD operations ## Technical Notes ### Database Schema ```php Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title_ar'); $table->string('title_en'); $table->text('body_ar'); $table->text('body_en'); $table->enum('status', ['draft', 'published'])->default('draft'); $table->timestamps(); }); ``` ### Post Model ```php class Post extends Model { protected $fillable = [ 'title_ar', 'title_en', 'body_ar', 'body_en', 'status', ]; public function getTitleAttribute(): string { $locale = app()->getLocale(); return $this->{"title_{$locale}"} ?? $this->title_en; } public function getBodyAttribute(): string { $locale = app()->getLocale(); return $this->{"body_{$locale}"} ?? $this->body_en; } public function getExcerptAttribute(): string { return Str::limit(strip_tags($this->body), 150); } public function scopePublished($query) { return $query->where('status', 'published'); } public function scopeDraft($query) { return $query->where('status', 'draft'); } } ``` ### Volt Component ```php exists) { $this->post = $post; $this->fill($post->only([ 'title_ar', 'title_en', 'body_ar', 'body_en', 'status' ])); } } public function save(): void { $validated = $this->validate([ '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'], ]); // Sanitize HTML $validated['body_ar'] = clean($validated['body_ar']); $validated['body_en'] = clean($validated['body_en']); if ($this->post) { $this->post->update($validated); $action = 'update'; } else { $this->post = Post::create($validated); $action = 'create'; } AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => $action, 'target_type' => 'post', 'target_id' => $this->post->id, 'ip_address' => request()->ip(), ]); session()->flash('success', __('messages.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->post && $this->status === 'draft') { $this->post->update([ 'title_ar' => $this->title_ar, 'title_en' => $this->title_en, 'body_ar' => clean($this->body_ar), 'body_en' => clean($this->body_en), ]); } } }; ``` ### Template with Rich Text Editor ```blade