Started working on invites
Some checks failed
linter / quality (push) Successful in 3m13s
tests / ci (push) Failing after 7m37s

This commit is contained in:
Javier Feliz 2025-08-01 20:59:42 -04:00
parent 8e0abedbbb
commit 1fd6f03a81
11 changed files with 280 additions and 4 deletions

View File

@ -21,6 +21,13 @@ class Register extends Component
public string $password_confirmation = '';
public string $code = '';
public function mount()
{
dd($this->code);
}
/**
* Handle an incoming registration request.
*/
@ -28,7 +35,7 @@ class Register extends Component
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
]);

View File

@ -0,0 +1,38 @@
<?php
namespace App\Livewire;
use App\Models\Invitation;
use App\Models\User;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class ManageUsers extends Component
{
public string $invite_email = '';
public Collection $users;
public Collection $invitations;
public function mount()
{
$this->users = User::all();
$this->invitations = Invitation::all();
}
public function inviteUser()
{
$inv = Invitation::create([
'code' => str()->random(50),
'email' => $this->invite_email,
'invited_by' => auth()->user()->id,
'expires_at' => now()->addDays(7),
]);
Flux::modal('invite-user')->close();
}
public function render()
{
return view('livewire.manage-users');
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class InvitationMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct()
{
//
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Invitation Mail',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'view.name',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

24
app/Models/Invitation.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Invitation extends Model
{
protected $guarded = ['id'];
protected $casts = [
'expires_at' => 'datetime',
'accepted_at' => 'datetime'
];
public function status(): string
{
return !empty($this->accepted_at) ? 'accepted' : 'pending';
}
public function isPending(): bool
{
return empty($this->accepted_at);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Database\Factories;
use App\Models\Invitation;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Invitation>
*/
class InvitationFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'code' => Invitation::generateCode(),
'email' => fake()->unique()->safeEmail(),
'invited_by' => User::factory(),
'expires_at' => now()->addDays(7),
'email_sent' => fake()->boolean(30),
];
}
/**
* Indicate that the invitation is expired.
*/
public function expired(): static
{
return $this->state(fn(array $attributes) => [
'expires_at' => now()->subDay(),
]);
}
/**
* Indicate that the invitation has been accepted.
*/
public function accepted(): static
{
return $this->state(fn(array $attributes) => [
'accepted_at' => now(),
'user_id' => User::factory(),
]);
}
/**
* Indicate that the email was sent.
*/
public function emailSent(): static
{
return $this->state(fn(array $attributes) => [
'email_sent' => true,
]);
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invitations', function (Blueprint $table) {
$table->id();
$table->string('code')->unique();
$table->string('email')->unique();
$table->foreignId('invited_by')->constrained('users')->onDelete('cascade');
$table->timestamp('expires_at');
$table->timestamp('accepted_at')->nullable();
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('cascade');
$table->boolean('email_sent')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invitations');
}
};

View File

@ -1,6 +1,4 @@
@props(['class' => ''])
<div {{ $attributes->merge(['class' => 'bg-white dark:bg-zinc-800 shadow-sm border border-gray-200 dark:border-zinc-700
rounded-lg ' . $class]) }}>
p-6'] ) }}>
{{ $slot }}
</div>

View File

@ -1,5 +1,8 @@
<x-layouts.app :title="__('Dashboard')">
<div class="max-w-4xl mx-auto py-12">
<div class="mb-4">
<livewire:manage-users />
</div>
<div class="grid grid-cols-2">
<livewire:forms.user-profile />
</div>

View File

@ -0,0 +1,52 @@
<div>
<div class="flex justify-between items-center">
<flux:heading size="xl">Users</flux:heading>
</div>
<flux:separator class="my-8" />
@foreach ($users as $u)
<x-card class="flex items-center justify-between p-6">
<div class="flex gap-4">
<flux:heading>{{$u->name}}</flux:heading>
<flux:text>{{$u->email}}</flux:text>
</div>
</x-card>
@endforeach
<div class="flex justify-between items-center mt-8">
<flux:heading size="xl">Invitations</flux:heading>
<div>
<flux:modal.trigger name="invite-user">
<flux:button variant="primary" icon="plus">Create</flux:button>
</flux:modal.trigger>
</div>
</div>
<flux:separator class="my-8" />
@foreach ($invitations as $inv)
<x-card class="flex items-center justify-between p-6">
<div class="flex gap-4 items-center flex-1">
<flux:heading>{{$inv->email}}</flux:heading>
@switch($inv->status())
@case('accepted')
<flux:badge color="green">Accepted</flux:badge>
@break
@case('pending')
<flux:badge>Pending</flux:badge>
@break
@default
<flux:badge>{{$inv->status}}</flux:badge>
@endswitch
</div>
<flux:text>Invite link: {{route('register', ['code' => $inv->code])}}</flux:text>
<div class="flex gap-4 items-center">
<flux:button variant="primary" size="sm">Copy invite link</flux:button>
</div>
</x-card>
@endforeach
<flux:modal name="invite-user" class="w-96">
<flux:heading>Invite User</flux:heading>
<flux:separator class="my-4" />
<form wire:submit="inviteUser" class="flex flex-col gap-4">
<flux:input label="Email" wire:model="invite_email" />
<flux:button type="submit" variant="primary">Create invitation</flux:button>
</form>
</flux:modal>
</div>

View File

@ -0,0 +1,7 @@
<?php
test('example', function () {
$response = $this->get('/');
$response->assertStatus(200);
});