feat: Phase 2 - page CRUD, subscriber management, user management

This commit is contained in:
2026-04-03 21:15:40 +02:00
parent 78e1be3e3b
commit cf026f46b0
33 changed files with 1135 additions and 82 deletions

View File

@@ -0,0 +1,55 @@
@php
/** @var \App\Models\User|null $user */
$user = $user ?? null;
$isEdit = $user !== null;
@endphp
<div class="grid max-w-xl gap-6">
<div>
<label for="name" class="block text-sm font-medium text-slate-700">{{ __('Name') }}</label>
<input type="text" name="name" id="name" value="{{ old('name', $user?->name) }}" required maxlength="255"
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
@error('name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="email" class="block text-sm font-medium text-slate-700">{{ __('Email') }}</label>
<input type="email" name="email" id="email" value="{{ old('email', $user?->email) }}" required
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
@error('email')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password" class="block text-sm font-medium text-slate-700">{{ __('Password') }}</label>
@if ($isEdit)
<p class="mt-0.5 text-xs text-slate-500">{{ __('Leave blank to keep the current password.') }}</p>
@endif
<input type="password" name="password" id="password" @if(! $isEdit) required @endif autocomplete="new-password"
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
@error('password')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password_confirmation" class="block text-sm font-medium text-slate-700">{{ __('Confirm password') }}</label>
<input type="password" name="password_confirmation" id="password_confirmation" @if(! $isEdit) required @endif autocomplete="new-password"
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
</div>
<div>
<label for="role" class="block text-sm font-medium text-slate-700">{{ __('Role') }}</label>
<select name="role" id="role" required
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="user" @selected(old('role', $user?->role) === 'user')>{{ __('User') }}</option>
<option value="superadmin" @selected(old('role', $user?->role) === 'superadmin')>{{ __('Superadmin') }}</option>
</select>
@error('role')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>

View File

@@ -5,8 +5,21 @@
@section('mobile_title', __('New user'))
@section('content')
<div class="mx-auto max-w-6xl">
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Create user') }}</h1>
<p class="mt-2 text-sm text-slate-600">{{ __('Form will be added in Step 10.') }}</p>
<div class="mx-auto max-w-xl">
<div class="mb-8">
<a href="{{ route('admin.users.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500"> {{ __('Back to users') }}</a>
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Create user') }}</h1>
</div>
<form action="{{ route('admin.users.store') }}" method="post" class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
@csrf
@include('admin.users._form', ['user' => null])
<div class="mt-8 flex gap-3">
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
{{ __('Create user') }}
</button>
<a href="{{ route('admin.users.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50">{{ __('Cancel') }}</a>
</div>
</form>
</div>
@endsection

View File

@@ -5,9 +5,23 @@
@section('mobile_title', __('Edit user'))
@section('content')
<div class="mx-auto max-w-6xl">
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Edit user') }}</h1>
<p class="mt-2 text-sm text-slate-600">{{ $user->email }}</p>
<p class="mt-2 text-sm text-slate-600">{{ __('Form will be added in Step 10.') }}</p>
<div class="mx-auto max-w-xl">
<div class="mb-8">
<a href="{{ route('admin.users.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500"> {{ __('Back to users') }}</a>
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Edit user') }}</h1>
<p class="mt-1 text-sm text-slate-600">{{ $user->email }}</p>
</div>
<form action="{{ route('admin.users.update', $user) }}" method="post" class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
@csrf
@method('PUT')
@include('admin.users._form', ['user' => $user])
<div class="mt-8 flex gap-3">
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
{{ __('Save changes') }}
</button>
<a href="{{ route('admin.users.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50">{{ __('Cancel') }}</a>
</div>
</form>
</div>
@endsection

View File

@@ -5,8 +5,64 @@
@section('mobile_title', __('Users'))
@section('content')
<div class="mx-auto max-w-6xl">
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Users') }}</h1>
<p class="mt-2 text-sm text-slate-600">{{ __('User management will be completed in a later step.') }}</p>
<div class="mx-auto max-w-5xl">
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Users') }}</h1>
<p class="mt-1 text-sm text-slate-600">{{ __('Backend accounts and roles.') }}</p>
</div>
@can('create', \App\Models\User::class)
<a href="{{ route('admin.users.create') }}"
class="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
{{ __('New user') }}
</a>
@endcan
</div>
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 text-left text-sm">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Name') }}</th>
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Email') }}</th>
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Role') }}</th>
<th class="px-4 py-3 text-right font-semibold text-slate-700">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@foreach ($users as $user)
<tr class="hover:bg-slate-50/80">
<td class="px-4 py-3 font-medium text-slate-900">{{ $user->name }}</td>
<td class="px-4 py-3 text-slate-600">{{ $user->email }}</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium {{ $user->role === 'superadmin' ? 'bg-violet-100 text-violet-800' : 'bg-slate-100 text-slate-700' }}">
{{ $user->role === 'superadmin' ? __('Superadmin') : __('User') }}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-right">
@can('update', $user)
<a href="{{ route('admin.users.edit', $user) }}" class="text-indigo-600 hover:text-indigo-500">{{ __('Edit') }}</a>
@endcan
@can('delete', $user)
<form action="{{ route('admin.users.destroy', $user) }}" method="post" class="ml-3 inline"
onsubmit="return confirm({{ json_encode(__('Delete this user? This cannot be undone.')) }});">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-500">{{ __('Delete') }}</button>
</form>
@endcan
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if ($users->hasPages())
<div class="border-t border-slate-200 px-4 py-3">
{{ $users->links() }}
</div>
@endif
</div>
</div>
@endsection