chore|feat: Instala Alpine.js e inicia a construção do formulário de criar usuários.

This commit is contained in:
lukibeg 2025-10-30 22:05:31 -03:00
parent 36377f1c05
commit e62e92f7bb
9 changed files with 272 additions and 21 deletions

View File

@ -18,6 +18,7 @@ public function createUsers(Request $request): RedirectResponse
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'email' => 'required|email|unique:users', 'email' => 'required|email|unique:users',
'password' => 'required|string|min:8', 'password' => 'required|string|min:8',
'password_confirm' => 'required|string|min:8'
]); ]);
try { try {

View File

@ -0,0 +1,85 @@
<?php
namespace App\Livewire\Admin;
use App\Services\UserService; // <-- Importe seu Service
use Livewire\Component;
class CreateUser extends Component
{
// 1. Propriedades públicas (os campos do formulário)
// Elas substituem o 'Request $request'
public string $name = '';
public string $email = '';
public string $password = '';
public string $password_confirm = ''; // <-- Você também precisa deste
public bool $permission_level = false;
// 2. As regras de validação (copiadas do seu controller)
// Nota: Adicionei 'same:password' para garantir que as senhas batem.
protected $rules = [
'name' => 'required|string|max:255',
'email' => 'required|email',
'password' => 'required|string|min:8',
'password_confirm' => 'required|string|same:password', // <-- Regra importante!
'permission_level' => 'required|boolean' // ou 'required|in:0,1'
];
protected $messages = [
'name' => 'Nome precisa ser informado.',
'email' => 'O email precisa ser informado.',
'password' => 'A senha precisa ter 8 ou mais caracteres.',
'password_confirm' => 'As senhas não coincidem.',
'permission_level' => 'Defina o nível de autorização do usuário.'
];
/**
* O método de "salvar", que substitui o seu 'createUsers'.
*
* Note como injetamos o UserService direto no método!
* O Livewire cuida disso para você, assim como o Laravel faz nos controllers.
*/
public function save(UserService $userService)
{
// 3. Valida as propriedades públicas ($this->name, $this->email, etc.)
$validated = $this->validate($this->rules, $this->messages);
// 4. Seu 'try...catch' - praticamente idêntico
try {
// 5. CHAMA O MESMO SERVICE! Nenhuma lógica de negócio é duplicada.
$user = $userService->createUser($validated);
// 6. O "Sucesso" (Tradução do Redirect)
// Limpa o formulário
$this->reset();
// Dispara um evento para o Alpine.js fechar o modal
$this->dispatch('user-created');
// Envia a mesma mensagem de sucesso do seu controller
session()->flash('message', 'Usuário cadastrado com sucesso!');
// (Opcional) Se sua tabela de usuários for outro componente Livewire,
// você pode mandar ela atualizar assim:
// $this->dispatch('refreshUserList');
} catch (\Exception $e) {
// 7. O "Erro" (Tradução do Redirect de Erro)
// Em vez de redirecionar, adicionamos o erro ao formulário
// para que o usuário veja na tela, sem refresh.
if ($e->getMessage() == 'O e-mail já está cadastrado.') {
$this->addError('email', $e->getMessage());
}
$this->addError('general', $e->getMessage());
}
}
public function render()
{
return view('livewire.admin.create-user');
}
}

View File

@ -11,6 +11,10 @@ class UserService
public function __construct(protected User $user) {} public function __construct(protected User $user) {}
public function createUser(array $user) public function createUser(array $user)
{ {
if (User::where('email', '=', $user['email'])) {
throw new \Exception('O e-mail já está cadastrado.');
}
return User::create($user); return User::create($user);
} }
} }

28
package-lock.json generated
View File

@ -4,7 +4,9 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nexus-linepbx", "dependencies": {
"alpinejs": "^3.15.1"
},
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"axios": "^1.11.0", "axios": "^1.11.0",
@ -1097,6 +1099,30 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/reactivity": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.1.5"
}
},
"node_modules/@vue/shared": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"license": "MIT"
},
"node_modules/alpinejs": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.1.tgz",
"integrity": "sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",

View File

@ -13,5 +13,8 @@
"laravel-vite-plugin": "^2.0.0", "laravel-vite-plugin": "^2.0.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"vite": "^7.0.4" "vite": "^7.0.4"
},
"dependencies": {
"alpinejs": "^3.15.1"
} }
} }

View File

@ -1,6 +1,14 @@
<h1>Dashboard</h1> @extends('layouts.app')
{{ dd($users) }}
@foreach ($users as $user)
<h1> {{ $user->name }}</h1>
@endforeach
@section('content')
<h1>Dashboard</h1>
<livewire:admin.create-user />
@foreach ($users as $user)
<h1> {{ $user->name }}</h1>
@endforeach
@endsection

View File

@ -5,12 +5,13 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title> @yield('title') </title> <title> @yield('title', 'Nexus') </title>
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net"> <link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
@vite('resources/css/app.css') @vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head> </head>
<header> <header>
@ -30,9 +31,7 @@
@endauth @endauth
@guest @guest
<a href="{{ route('users.create') }}">Início</a> <div>Bem vindo ao Nexus.</div>
<a href="{{ route('login') }}">Contato</a>
<a href="{{ route('users.create') }}">Sobre</a>
@endguest @endguest
</nav> </nav>
@ -40,8 +39,7 @@
<body> <body>
@yield('content') @yield('content')
@livewireScripts
@vite('resources/js/app.js')
</body> </body>

View File

@ -0,0 +1,126 @@
<div x-data="{ showModal: false }" x-on:create-user.window="showModal = false">
<button x-on:click="showModal = true" class="px-4 py-2 bg-blue-600 text-white rounded ...">
Criar Novo Usuário
</button>
@if (session('message'))
<div class="p-3 bg-green-100 text-green-700 rounded mb-4">
{{ session('message') }}
</div>
@endif
<div x-show="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-white/35"
style="display: none;">
<div x-on:click.outside="showModal = false" class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6">
<h3 class="text-lg font-medium text-gray-900">Novo Usuário Nexus</h3>
<form wire:submit.prevent="save" class="mt-4 space-y-4">
@error('general')
<div class="p-3 bg-red-100 text-red-700 rounded">
<strong>Erro:</strong> {{ $message }}
</div>
@enderror
<div>
<label for="name" class="block text-sm ...">Nome</label>
<input type="text" wire:model="name" id="name"
class="mt-1 block w-full border-2 border-gray-200 rounded-md outline-none hover:border-blue-200 transition delay-150 duration-300 ease-in-out focus:border-blue-200">
@error('name') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="email" class="block text-sm ...">Email</label>
<input type="email" wire:model="email" id="email"
class="mt-1 block w-full border-2 border-gray-200 rounded-md outline-none hover:border-blue-200 transition delay-150 duration-300 ease-in-out focus:border-blue-200">
@error('email') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="password" class="block text-sm ...">Senha</label>
<input type="password" wire:model="password" id="password"
class="mt-1 block w-full border-2 border-gray-200 rounded-md outline-none hover:border-blue-200 transition delay-150 duration-300 ease-in-out focus:border-blue-200">
@error('password') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="password_confirm" class="block text-sm ...">Confirmar Senha</label>
<input type="password" wire:model="password_confirm" id="password_confirm"
class="mt-1 block w-full border-2 border-gray-200 rounded-md outline-none hover:border-blue-200 transition delay-150 duration-300 ease-in-out focus:border-blue-200">
@error('password_confirm') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label for="permissions" class="block text-sm font-medium text-gray-700">Permissões</label>
<div
x-data="{
open: false,
selected: @entangle('permission_level'),
options: { '0': 'Usuário', '1': 'Admin' }
}"
x-on:click.outside="open = false" class="relative mt-1"
>
<button
type="button"
x-on:click="open = true" class="relative h-10 w-40 cursor-default rounded-lg border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-sm p-2"
>
<span class="block truncate" x-text="options[selected]"></span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a.75.75 0 01.53.22l3.5 3.5a.75.75 0 01-1.06 1.06L10 4.81 6.53 8.28a.75.75 0 01-1.06-1.06l3.5-3.5A.75.75 0 0110 3zm-3.5 9.28a.75.75 0 011.06 0L10 15.19l3.47-3.47a.75.75 0 111.06 1.06l-3.5 3.5a.75.75 0 01-1.06 0l-3.5-3.5a.75.75 0 010-1.06z" clip-rule="evenodd" />
</svg>
</span>
</button>
<div
x-show="open"
x-transition
style="display: none;"
class="absolute z-10 mt-1 w-full max-h-60 overflow-auto rounded-md bg-white py-1 shadow-lg"
>
<div
x-on:click="selected = '0'; open = false" class="relative cursor-default select-none py-2 pl-10 pr-4 text-gray-900 hover:bg-blue-100"
:class="{ 'bg-blue-50': selected == '0' }" >
<span class="block truncate" :class="{ 'font-semibold': selected == '0' }">Usuário</span>
<span x-show="selected == '0'" class="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-600">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
</svg>
</span>
</div>
<div
x-on:click="selected = '1'; open = false"
class="relative cursor-default select-none py-2 pl-10 pr-4 text-gray-900 hover:bg-blue-100"
:class="{ 'bg-blue-50': selected == '1' }"
>
<span class="block truncate" :class="{ 'font-semibold': selected == '1' }">Admin</span>
<span x-show="selected == '1'" class="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-600">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
</svg>
</span>
</div>
</div>
</div>
@error('permission_level') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" @click="showModal = false" class="px-4 py-2 rounded-md bg-gray-200 ...">
Cancelar
</button>
<button type="submit" class="px-4 py-2 bg-blue-600 rounded-md text-white ...">
<span wire:loading.remove wire:target="save">Salvar</span>
<span wire:loading wire:target="save">Salvando...</span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -6,6 +6,7 @@
use App\Http\Controllers\CreateUserController; use App\Http\Controllers\CreateUserController;
use App\Http\Controllers\LoginController; use App\Http\Controllers\LoginController;
use App\Http\Controllers\LogoutController; use App\Http\Controllers\LogoutController;
use App\Livewire\Counter;
@ -29,7 +30,6 @@
})->name('users.create')->middleware('authorization'); })->name('users.create')->middleware('authorization');
}); });
Route::controller(LoginController::class)->group(function () { Route::controller(LoginController::class)->group(function () {
Route::get('/login', function () { Route::get('/login', function () {
return view('login'); return view('login');