Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions app/Http/Controllers/Admin/UserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace HopsWeb\Http\Controllers\Admin;

use HopsWeb\Http\Controllers\Controller;
use HopsWeb\Http\Requests\Admin\UpdateUserRequest;
use HopsWeb\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class UserController extends Controller
{
use AuthorizesRequests;

public function index(Request $request): View
{
$this->authorize("viewAny", User::class);

$query = User::query();

if ($request->filled("search")) {
$search = $request->input("search");
$query->where(function ($q) use ($search): void {
$q->where("name", "like", "%" . $search . "%")
->orWhere("email", "like", "%" . $search . "%");
});
}

$users = $query->latest()->paginate(20)->withQueryString();

return view("admin.users.index", compact("users"));
}

public function edit(User $user): View
{
$this->authorize("update", $user);

return view("admin.users.edit", compact("user"));
}

public function update(UpdateUserRequest $request, User $user): RedirectResponse
{
$user->update($request->validated());

return redirect()->route("admin.users.index");
}

public function destroy(User $user): RedirectResponse
{
$this->authorize("delete", $user);

$user->delete();

return redirect()->route("admin.users.index");
}
}
31 changes: 31 additions & 0 deletions app/Http/Requests/Admin/UpdateUserRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace HopsWeb\Http\Requests\Admin;

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can("update", $this->route("user"));
}

public function rules(): array
{
return [
"is_admin" => ["boolean"],
"is_team_member" => ["boolean"],
];
}

protected function prepareForValidation(): void
{
$this->merge([
"is_admin" => $this->boolean("is_admin"),
"is_team_member" => $this->boolean("is_team_member"),
]);
}
}
25 changes: 25 additions & 0 deletions app/Policies/UserPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace HopsWeb\Policies;

use HopsWeb\Models\User;

class UserPolicy
{
public function viewAny(User $user): bool
{
return $user->is_admin;
}

public function update(User $user, User $model): bool
{
return $user->is_admin;
}

public function delete(User $user, User $model): bool
{
return $user->is_admin && $user->id !== $model->id;
}
}
6 changes: 5 additions & 1 deletion database/seeders/DemoSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ class DemoSeeder extends Seeder
public function run(): void
{
Hop::factory(50)->create();
User::factory(10)->create();
User::factory(30)->create();

User::factory(["name" => "Admin", "email" => "admin@example.com", "password" => "password"])->admin()->create();
User::factory(["name" => "Team Member", "email" => "member@example.com", "password" => "password"])->teamMember()->create();

HopQuery::factory(10)->for(User::first())->create();
}
}
41 changes: 41 additions & 0 deletions resources/views/admin/users/edit.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit User: ') }} {{ $user->name }}
</h2>
</x-slot>

<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<form method="POST" action="{{ route('admin.users.update', $user) }}">
@csrf
@method('PUT')

<div class="mb-4">
<label class="inline-flex items-center">
<input type="checkbox" name="is_admin" value="1" {{ $user->is_admin ? 'checked' : '' }} class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500">
<span class="ml-2 text-sm text-gray-600">{{ __('Admin') }}</span>
</label>
</div>

<div class="mb-4">
<label class="inline-flex items-center">
<input type="checkbox" name="is_team_member" value="1" {{ $user->is_team_member ? 'checked' : '' }} class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500">
<span class="ml-2 text-sm text-gray-600">{{ __('Team Member') }}</span>
</label>
<x-input-error :messages="$errors->get('is_team_member')" class="mt-2" />
<x-input-error :messages="$errors->get('is_admin')" class="mt-2" />
</div>

<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
<a href="{{ route('admin.users.index') }}" class="text-sm text-gray-600 hover:text-gray-900">{{ __('Cancel') }}</a>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
77 changes: 77 additions & 0 deletions resources/views/admin/users/index.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Users') }}
</h2>
</x-slot>

<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<div class="mb-4 flex justify-between items-center">
<form method="GET" action="{{ route('admin.users.index') }}" class="flex items-center gap-2">
<x-text-input name="search" value="{{ request('search') }}" placeholder="Search users..." />
<x-primary-button>Search</x-primary-button>
</form>
</div>

<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-6 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Name</th>
<th class="px-6 py-6 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Email</th>
<th class="px-6 py-6 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Admin</th>
<th class="px-6 py-6 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Team Member</th>
<th class="px-6 py-6 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Created At</th>
<th class="px-6 py-6 text-right text-xs font-semibold text-gray-700 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach ($users as $user)
<tr>
<td class="px-6 py-4 whitespace-nowrap">{{ $user->name }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ $user->email }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $user->is_admin ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
{{ $user->is_admin ? 'Yes' : 'No' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $user->is_team_member ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800' }}">
{{ $user->is_team_member ? 'Yes' : 'No' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $user->created_at->format('Y-m-d H:i') }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end gap-2">
<a href="{{ route('admin.users.edit', $user) }}" class="inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150">
{{ __('Edit') }}
</a>
@if (auth()->id() !== $user->id)
<form action="{{ route('admin.users.destroy', $user) }}" method="POST" class="inline-block" onsubmit="return confirm('Are you sure?')">
@csrf
@method('DELETE')
<x-danger-button type="submit">
{{ __('Delete') }}
</x-danger-button>
</form>
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>

<div class="mt-4">
{{ $users->links() }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
10 changes: 9 additions & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

declare(strict_types=1);

use HopsWeb\Http\Controllers\Admin\UserController;
use HopsWeb\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get("/", fn() => view("welcome"));

Route::get("/dashboard", fn() => view("dashboard"))->middleware(["auth", "verified"])->name("dashboard");

Route::middleware("auth")->group(function (): void {
Route::middleware(["auth"])->group(function (): void {
Route::get("/profile", [ProfileController::class, "edit"])->name("profile.edit");
Route::patch("/profile", [ProfileController::class, "update"])->name("profile.update");
Route::delete("/profile", [ProfileController::class, "destroy"])->name("profile.destroy");

Route::prefix("admin")->name("admin.")->group(function (): void {
Route::get("/users", [UserController::class, "index"])->name("users.index");
Route::get("/users/{user}/edit", [UserController::class, "edit"])->name("users.edit");
Route::put("/users/{user}", [UserController::class, "update"])->name("users.update");
Route::delete("/users/{user}", [UserController::class, "destroy"])->name("users.destroy");
});
});