feat: add Sites Livewire CRUD page
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Sites;
|
||||||
|
|
||||||
|
use App\Models\Site;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('components.layouts.app')]
|
||||||
|
class Index extends Component
|
||||||
|
{
|
||||||
|
public bool $showForm = false;
|
||||||
|
|
||||||
|
public ?int $editingId = null;
|
||||||
|
|
||||||
|
public string $name = '';
|
||||||
|
|
||||||
|
public string $url = '';
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.sites.index', [
|
||||||
|
'sites' => Site::orderBy('name')->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): void
|
||||||
|
{
|
||||||
|
$this->resetForm();
|
||||||
|
$this->showForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(int $id): void
|
||||||
|
{
|
||||||
|
$site = Site::findOrFail($id);
|
||||||
|
$this->editingId = $site->id;
|
||||||
|
$this->name = $site->name;
|
||||||
|
$this->url = $site->url;
|
||||||
|
$this->description = $site->description;
|
||||||
|
$this->showForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'url' => ['required', 'string', 'max:255'],
|
||||||
|
'description' => ['nullable', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Site::updateOrCreate(['id' => $this->editingId], $validated);
|
||||||
|
|
||||||
|
$this->resetForm();
|
||||||
|
$this->showForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $id): void
|
||||||
|
{
|
||||||
|
Site::findOrFail($id)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetForm(): void
|
||||||
|
{
|
||||||
|
$this->editingId = null;
|
||||||
|
$this->name = '';
|
||||||
|
$this->url = '';
|
||||||
|
$this->description = null;
|
||||||
|
$this->resetValidation();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h1 class="text-xl font-semibold">Sites</h1>
|
||||||
|
<button wire:click="create" class="bg-blue-600 text-white px-4 py-2 rounded">Add Site</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">Name</th>
|
||||||
|
<th class="p-3">URL</th>
|
||||||
|
<th class="p-3">Description</th>
|
||||||
|
<th class="p-3"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($sites as $site)
|
||||||
|
<tr wire:key="site-{{ $site->id }}" class="border-b border-gray-100">
|
||||||
|
<td class="p-3">{{ $site->name }}</td>
|
||||||
|
<td class="p-3"><a href="{{ $site->url }}" target="_blank" class="text-blue-600">{{ $site->url }}</a></td>
|
||||||
|
<td class="p-3">{{ $site->description }}</td>
|
||||||
|
<td class="p-3 text-right space-x-2">
|
||||||
|
<button wire:click="edit({{ $site->id }})" class="text-blue-600">Edit</button>
|
||||||
|
<button wire:click="delete({{ $site->id }})" wire:confirm="Delete this site?" 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 Site' : 'Add Site' }}</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Name</label>
|
||||||
|
<input type="text" wire:model="name" class="w-full border rounded p-2">
|
||||||
|
@error('name') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">URL</label>
|
||||||
|
<input type="text" wire:model="url" class="w-full border rounded p-2">
|
||||||
|
@error('url') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Description</label>
|
||||||
|
<textarea wire:model="description" class="w-full border rounded p-2"></textarea>
|
||||||
|
</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>
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Sites\Index as SitesIndex;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return view('welcome');
|
return view('welcome');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('/sites', SitesIndex::class)->name('sites.index');
|
||||||
|
|
||||||
|
// placeholder, replaced in Task 6
|
||||||
|
Route::get('/social-accounts', SitesIndex::class)->name('social-accounts.index');
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Sites\Index;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
test('sites index page lists existing sites', function () {
|
||||||
|
$site = Site::factory()->create(['name' => 'Example Site']);
|
||||||
|
|
||||||
|
$this->get(route('sites.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Example Site');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a new site can be created', function () {
|
||||||
|
Livewire::test(Index::class)
|
||||||
|
->set('name', 'New Site')
|
||||||
|
->set('url', 'https://newsite.test')
|
||||||
|
->set('description', 'Some description')
|
||||||
|
->call('save');
|
||||||
|
|
||||||
|
expect(Site::where('name', 'New Site')->exists())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('name and url are required to create a site', function () {
|
||||||
|
Livewire::test(Index::class)
|
||||||
|
->set('name', '')
|
||||||
|
->set('url', '')
|
||||||
|
->call('save')
|
||||||
|
->assertHasErrors(['name', 'url']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an existing site can be edited', function () {
|
||||||
|
$site = Site::factory()->create(['name' => 'Old Name']);
|
||||||
|
|
||||||
|
Livewire::test(Index::class)
|
||||||
|
->call('edit', $site->id)
|
||||||
|
->set('name', 'Updated Name')
|
||||||
|
->call('save');
|
||||||
|
|
||||||
|
expect($site->fresh()->name)->toBe('Updated Name');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a site can be deleted', function () {
|
||||||
|
$site = Site::factory()->create();
|
||||||
|
|
||||||
|
Livewire::test(Index::class)->call('delete', $site->id);
|
||||||
|
|
||||||
|
expect(Site::find($site->id))->toBeNull();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user