feat: add Social Accounts Livewire CRUD page
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3f46eecff8
commit
cee5b1976b
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\SocialAccounts;
|
||||
|
||||
use App\Models\SocialAccount;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Layout('components.layouts.app')]
|
||||
class Index extends Component
|
||||
{
|
||||
public bool $showForm = false;
|
||||
|
||||
public ?int $editingId = null;
|
||||
|
||||
public ?int $revealedId = null;
|
||||
|
||||
public string $network = '';
|
||||
|
||||
public string $username = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public string $password = '';
|
||||
|
||||
public ?string $type = null;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.social-accounts.index', [
|
||||
'accounts' => SocialAccount::orderBy('username')->get(),
|
||||
'networks' => SocialAccount::NETWORKS,
|
||||
'types' => SocialAccount::TYPES,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->resetForm();
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
public function edit(int $id): void
|
||||
{
|
||||
$account = SocialAccount::findOrFail($id);
|
||||
$this->editingId = $account->id;
|
||||
$this->network = $account->network;
|
||||
$this->username = $account->username;
|
||||
$this->email = $account->email;
|
||||
$this->password = $account->password;
|
||||
$this->type = $account->type;
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'network' => ['required', 'string', 'in:' . implode(',', SocialAccount::NETWORKS)],
|
||||
'username' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'max:255'],
|
||||
'password' => ['required', 'string', 'max:255'],
|
||||
'type' => ['nullable', 'string', 'in:' . implode(',', SocialAccount::TYPES)],
|
||||
]);
|
||||
|
||||
SocialAccount::updateOrCreate(['id' => $this->editingId], $validated);
|
||||
|
||||
$this->resetForm();
|
||||
$this->showForm = false;
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
SocialAccount::findOrFail($id)->delete();
|
||||
}
|
||||
|
||||
public function toggleReveal(int $id): void
|
||||
{
|
||||
$this->revealedId = $this->revealedId === $id ? null : $id;
|
||||
}
|
||||
|
||||
public function resetForm(): void
|
||||
{
|
||||
$this->editingId = null;
|
||||
$this->network = '';
|
||||
$this->username = '';
|
||||
$this->email = '';
|
||||
$this->password = '';
|
||||
$this->type = null;
|
||||
$this->resetValidation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-xl font-semibold">Social Accounts</h1>
|
||||
<button wire:click="create" class="bg-blue-600 text-white px-4 py-2 rounded">Add Account</button>
|
||||
</div>
|
||||
|
||||
<table class="w-full bg-white border border-gray-200 rounded">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-200">
|
||||
<th class="p-3">Network</th>
|
||||
<th class="p-3">Username</th>
|
||||
<th class="p-3">Email</th>
|
||||
<th class="p-3">Password</th>
|
||||
<th class="p-3">Type</th>
|
||||
<th class="p-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($accounts as $account)
|
||||
<tr wire:key="account-{{ $account->id }}" class="border-b border-gray-100">
|
||||
<td class="p-3">{{ ucfirst($account->network) }}</td>
|
||||
<td class="p-3">{{ $account->username }}</td>
|
||||
<td class="p-3">{{ $account->email }}</td>
|
||||
<td class="p-3">
|
||||
@if ($revealedId === $account->id)
|
||||
{{ $account->password }}
|
||||
@else
|
||||
••••••••
|
||||
@endif
|
||||
<button wire:click="toggleReveal({{ $account->id }})" class="text-xs text-blue-600 ml-2">
|
||||
{{ $revealedId === $account->id ? 'Hide' : 'Show' }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="p-3">{{ $account->type ? ucfirst($account->type) : '—' }}</td>
|
||||
<td class="p-3 text-right space-x-2">
|
||||
<button wire:click="edit({{ $account->id }})" class="text-blue-600">Edit</button>
|
||||
<button wire:click="delete({{ $account->id }})" wire:confirm="Delete this account?" class="text-red-600">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if ($showForm)
|
||||
<div class="fixed inset-0 bg-black/30 flex items-center justify-center">
|
||||
<div class="bg-white p-6 rounded w-full max-w-md space-y-4">
|
||||
<h2 class="text-lg font-semibold">{{ $editingId ? 'Edit Account' : 'Add Account' }}</h2>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Network</label>
|
||||
<select wire:model="network" class="w-full border rounded p-2">
|
||||
<option value="">Select network</option>
|
||||
@foreach ($networks as $network)
|
||||
<option value="{{ $network }}">{{ ucfirst($network) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('network') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Username</label>
|
||||
<input type="text" wire:model="username" class="w-full border rounded p-2">
|
||||
@error('username') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Email</label>
|
||||
<input type="text" wire:model="email" class="w-full border rounded p-2">
|
||||
@error('email') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Password</label>
|
||||
<input type="text" wire:model="password" class="w-full border rounded p-2">
|
||||
@error('password') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Type</label>
|
||||
<select wire:model="type" class="w-full border rounded p-2">
|
||||
<option value="">None</option>
|
||||
@foreach ($types as $t)
|
||||
<option value="{{ $t }}">{{ ucfirst($t) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('type') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button wire:click="$set('showForm', false)" class="px-4 py-2">Cancel</button>
|
||||
<button wire:click="save" class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
+2
-3
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Sites\Index as SitesIndex;
|
||||
use App\Livewire\SocialAccounts\Index as SocialAccountsIndex;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
@@ -8,6 +9,4 @@ Route::get('/', function () {
|
||||
});
|
||||
|
||||
Route::get('/sites', SitesIndex::class)->name('sites.index');
|
||||
|
||||
// placeholder, replaced in Task 6
|
||||
Route::get('/social-accounts', SitesIndex::class)->name('social-accounts.index');
|
||||
Route::get('/social-accounts', SocialAccountsIndex::class)->name('social-accounts.index');
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\SocialAccounts\Index;
|
||||
use App\Models\SocialAccount;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('social accounts index page lists existing accounts', function () {
|
||||
SocialAccount::factory()->create(['username' => 'example_user']);
|
||||
|
||||
$this->get(route('social-accounts.index'))
|
||||
->assertOk()
|
||||
->assertSee('example_user');
|
||||
});
|
||||
|
||||
test('a new social account can be created', function () {
|
||||
Livewire::test(Index::class)
|
||||
->set('network', 'twitter')
|
||||
->set('username', 'new_user')
|
||||
->set('email', 'new_user@example.com')
|
||||
->set('password', 'secret123')
|
||||
->set('type', 'post')
|
||||
->call('save');
|
||||
|
||||
expect(SocialAccount::where('username', 'new_user')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('network, username, email, and password are required', function () {
|
||||
Livewire::test(Index::class)
|
||||
->set('network', '')
|
||||
->set('username', '')
|
||||
->set('email', '')
|
||||
->set('password', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['network', 'username', 'email', 'password']);
|
||||
});
|
||||
|
||||
test('type can be left null', function () {
|
||||
Livewire::test(Index::class)
|
||||
->set('network', 'reddit')
|
||||
->set('username', 'no_type_user')
|
||||
->set('email', 'no_type@example.com')
|
||||
->set('password', 'secret123')
|
||||
->set('type', null)
|
||||
->call('save');
|
||||
|
||||
$account = SocialAccount::where('username', 'no_type_user')->first();
|
||||
expect($account->type)->toBeNull();
|
||||
});
|
||||
|
||||
test('an existing account can be edited', function () {
|
||||
$account = SocialAccount::factory()->create(['username' => 'old_name']);
|
||||
|
||||
Livewire::test(Index::class)
|
||||
->call('edit', $account->id)
|
||||
->set('username', 'new_name')
|
||||
->call('save');
|
||||
|
||||
expect($account->fresh()->username)->toBe('new_name');
|
||||
});
|
||||
|
||||
test('an account can be deleted', function () {
|
||||
$account = SocialAccount::factory()->create();
|
||||
|
||||
Livewire::test(Index::class)->call('delete', $account->id);
|
||||
|
||||
expect(SocialAccount::find($account->id))->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user