diff --git a/app/Livewire/Posts/Index.php b/app/Livewire/Posts/Index.php new file mode 100644 index 0000000..bf6c9ee --- /dev/null +++ b/app/Livewire/Posts/Index.php @@ -0,0 +1,106 @@ + Post::with(['site', 'socialAccount'])->orderByDesc('created_at')->get(), + 'sites' => Site::orderBy('name')->get(), + 'accounts' => SocialAccount::orderBy('username')->get(), + 'types' => Post::TYPES, + 'statuses' => Post::STATUSES, + ]); + } + + public function create(): void + { + $this->resetForm(); + $this->showForm = true; + } + + public function edit(int $id): void + { + $post = Post::findOrFail($id); + $this->editingId = $post->id; + $this->siteId = (string) $post->site_id; + $this->socialAccountId = $post->social_account_id ? (string) $post->social_account_id : ''; + $this->type = $post->type; + $this->title = (string) $post->title; + $this->body = $post->body; + $this->status = $post->status; + $this->scheduledAt = $post->scheduled_at?->format('Y-m-d\TH:i') ?? ''; + $this->showForm = true; + } + + public function save(): void + { + $validated = $this->validate([ + 'siteId' => ['required', 'integer', 'exists:sites,id'], + 'socialAccountId' => ['nullable', 'integer', 'exists:social_accounts,id'], + 'type' => ['required', 'string', 'in:' . implode(',', Post::TYPES)], + 'title' => ['nullable', 'string', 'max:255'], + 'body' => ['required', 'string'], + 'status' => ['required', 'string', 'in:' . implode(',', Post::STATUSES)], + 'scheduledAt' => ['nullable', 'date'], + ]); + + Post::updateOrCreate(['id' => $this->editingId], [ + 'site_id' => $validated['siteId'], + 'social_account_id' => $validated['socialAccountId'] ?: null, + 'type' => $validated['type'], + 'title' => $validated['title'] ?: null, + 'body' => $validated['body'], + 'status' => $validated['status'], + 'scheduled_at' => $validated['scheduledAt'] ?: null, + ]); + + $this->resetForm(); + $this->showForm = false; + } + + public function delete(int $id): void + { + Post::findOrFail($id)->delete(); + } + + public function resetForm(): void + { + $this->editingId = null; + $this->siteId = ''; + $this->socialAccountId = ''; + $this->type = ''; + $this->title = ''; + $this->body = ''; + $this->status = 'draft'; + $this->scheduledAt = ''; + $this->resetValidation(); + } +} diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index 220a5f2..af69a50 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -11,6 +11,7 @@
diff --git a/resources/views/livewire/posts/index.blade.php b/resources/views/livewire/posts/index.blade.php new file mode 100644 index 0000000..a631068 --- /dev/null +++ b/resources/views/livewire/posts/index.blade.php @@ -0,0 +1,110 @@ +
+
+

Posts

+ +
+ + + + + + + + + + + + + + + @foreach ($posts as $post) + + + + + + + + + + @endforeach + +
SiteTypeTitleStatusScheduledAccount
{{ $post->site->name }}{{ ucfirst($post->type) }}{{ $post->title ?: \Illuminate\Support\Str::limit($post->body, 40) }}{{ ucfirst($post->status) }}{{ $post->scheduled_at?->format('Y-m-d H:i') ?? '—' }}{{ $post->socialAccount?->username ?? '—' }} + + +
+ + @if ($showForm) +
+
+

{{ $editingId ? 'Edit Post' : 'Add Post' }}

+ +
+ + + @error('siteId') {{ $message }} @enderror +
+ +
+ + + @error('socialAccountId') {{ $message }} @enderror +
+ +
+ + + @error('type') {{ $message }} @enderror +
+ +
+ + + @error('title') {{ $message }} @enderror +
+ +
+ + + @error('body') {{ $message }} @enderror +
+ +
+ + + @error('status') {{ $message }} @enderror +
+ +
+ + + @error('scheduledAt') {{ $message }} @enderror +
+ +
+ + +
+
+
+ @endif +
diff --git a/routes/web.php b/routes/web.php index e8729ff..7bf6a79 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ name('sites.index'); Route::get('/social-accounts', SocialAccountsIndex::class)->name('social-accounts.index'); +Route::get('/posts', PostsIndex::class)->name('posts.index'); diff --git a/tests/Feature/Livewire/PostsTest.php b/tests/Feature/Livewire/PostsTest.php new file mode 100644 index 0000000..ad91dd5 --- /dev/null +++ b/tests/Feature/Livewire/PostsTest.php @@ -0,0 +1,81 @@ +create(['name' => 'My Blog']); + Post::factory()->create(['site_id' => $site->id, 'title' => 'Hello World Post']); + + $this->get(route('posts.index')) + ->assertOk() + ->assertSee('Hello World Post') + ->assertSee('My Blog'); +}); + +test('a new post can be created with required fields only', function () { + $site = Site::factory()->create(); + + Livewire::test(Index::class) + ->set('siteId', $site->id) + ->set('type', 'blog') + ->set('body', 'New post body') + ->call('save'); + + $post = Post::where('body', 'New post body')->first(); + expect($post)->not->toBeNull(); + expect($post->site_id)->toBe($site->id); + expect($post->status)->toBe('draft'); +}); + +test('a new post can be created with an account, title, status, and schedule', function () { + $site = Site::factory()->create(); + $account = SocialAccount::factory()->create(); + + Livewire::test(Index::class) + ->set('siteId', $site->id) + ->set('socialAccountId', $account->id) + ->set('type', 'twitter') + ->set('title', 'Launch') + ->set('body', 'We launched') + ->set('status', 'scheduled') + ->set('scheduledAt', '2026-08-01T09:00') + ->call('save'); + + $post = Post::where('title', 'Launch')->first(); + expect($post->social_account_id)->toBe($account->id); + expect($post->status)->toBe('scheduled'); + expect($post->scheduled_at)->not->toBeNull(); +}); + +test('site, type, and body are required to create a post', function () { + Livewire::test(Index::class) + ->set('siteId', '') + ->set('type', '') + ->set('body', '') + ->call('save') + ->assertHasErrors(['siteId', 'type', 'body']); +}); + +test('an existing post can be edited', function () { + $site = Site::factory()->create(); + $post = Post::factory()->create(['site_id' => $site->id, 'title' => 'Old Title']); + + Livewire::test(Index::class) + ->call('edit', $post->id) + ->set('title', 'New Title') + ->call('save'); + + expect($post->fresh()->title)->toBe('New Title'); +}); + +test('a post can be deleted', function () { + $post = Post::factory()->create(); + + Livewire::test(Index::class)->call('delete', $post->id); + + expect(Post::find($post->id))->toBeNull(); +});