diff --git a/app/Livewire/Auth/Register.php b/app/Livewire/Auth/Register.php index 8541536..fb459e5 100644 --- a/app/Livewire/Auth/Register.php +++ b/app/Livewire/Auth/Register.php @@ -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()], ]); diff --git a/app/Livewire/ManageUsers.php b/app/Livewire/ManageUsers.php new file mode 100644 index 0000000..ceff231 --- /dev/null +++ b/app/Livewire/ManageUsers.php @@ -0,0 +1,38 @@ +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'); + } +} diff --git a/app/Mail/InvitationMail.php b/app/Mail/InvitationMail.php new file mode 100644 index 0000000..ca6deeb --- /dev/null +++ b/app/Mail/InvitationMail.php @@ -0,0 +1,53 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php new file mode 100644 index 0000000..c810148 --- /dev/null +++ b/app/Models/Invitation.php @@ -0,0 +1,24 @@ + 'datetime', + 'accepted_at' => 'datetime' + ]; + + public function status(): string + { + return !empty($this->accepted_at) ? 'accepted' : 'pending'; + } + + public function isPending(): bool + { + return empty($this->accepted_at); + } +} diff --git a/database/factories/InvitationFactory.php b/database/factories/InvitationFactory.php new file mode 100644 index 0000000..635d6b6 --- /dev/null +++ b/database/factories/InvitationFactory.php @@ -0,0 +1,60 @@ + + */ +class InvitationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + 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, + ]); + } +} diff --git a/database/migrations/2025_07_31_084336_create_invitations_table.php b/database/migrations/2025_07_31_084336_create_invitations_table.php new file mode 100644 index 0000000..e167f36 --- /dev/null +++ b/database/migrations/2025_07_31_084336_create_invitations_table.php @@ -0,0 +1,34 @@ +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'); + } +}; diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php index 21e9fbf..a76b889 100644 --- a/resources/views/components/card.blade.php +++ b/resources/views/components/card.blade.php @@ -1,6 +1,4 @@ -@props(['class' => '']) -
merge(['class' => 'bg-white dark:bg-zinc-800 shadow-sm border border-gray-200 dark:border-zinc-700 - rounded-lg ' . $class]) }}> + p-6'] ) }}> {{ $slot }}
\ No newline at end of file diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 84f64f4..bffac9d 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,5 +1,8 @@
+
+ +
diff --git a/resources/views/emails/invitation.blade.php b/resources/views/emails/invitation.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/livewire/manage-users.blade.php b/resources/views/livewire/manage-users.blade.php new file mode 100644 index 0000000..be11430 --- /dev/null +++ b/resources/views/livewire/manage-users.blade.php @@ -0,0 +1,52 @@ +
+
+ Users +
+ + @foreach ($users as $u) + +
+ {{$u->name}} + {{$u->email}} +
+
+ @endforeach +
+ Invitations +
+ + Create + +
+
+ + @foreach ($invitations as $inv) + +
+ {{$inv->email}} + @switch($inv->status()) + @case('accepted') + Accepted + @break + @case('pending') + Pending + @break + @default + {{$inv->status}} + @endswitch +
+ Invite link: {{route('register', ['code' => $inv->code])}} +
+ Copy invite link +
+
+ @endforeach + + Invite User + +
+ + Create invitation + +
+
\ No newline at end of file diff --git a/tests/Feature/InvitationSystemTest.php b/tests/Feature/InvitationSystemTest.php new file mode 100644 index 0000000..b46239f --- /dev/null +++ b/tests/Feature/InvitationSystemTest.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); +});