From 6ef67faed45ce73aefefac53da635ff6a5e75409 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Anna-Sara=20S=C3=A9lea?=
Date: Fri, 26 Sep 2025 18:45:34 +0200
Subject: [PATCH] Breeze setup
---
.../Auth/AuthenticatedSessionController.php | 52 +++
.../Auth/ConfirmablePasswordController.php | 41 ++
...mailVerificationNotificationController.php | 24 ++
.../EmailVerificationPromptController.php | 22 ++
.../Auth/NewPasswordController.php | 69 ++++
.../Controllers/Auth/PasswordController.php | 29 ++
.../Auth/PasswordResetLinkController.php | 51 +++
.../Auth/RegisteredUserController.php | 51 +++
.../Auth/VerifyEmailController.php | 27 ++
app/Http/Controllers/ProfileController.php | 63 +++
app/Http/Middleware/HandleInertiaRequests.php | 39 ++
app/Http/Requests/Auth/LoginRequest.php | 85 +++++
app/Http/Requests/ProfileUpdateRequest.php | 30 ++
app/Providers/AppServiceProvider.php | 3 +-
bootstrap/app.php | 5 +
composer.json | 6 +-
composer.lock | 267 ++++++++++++-
jsconfig.json | 10 +
package.json | 10 +-
postcss.config.js | 6 +
resources/css/app.css | 14 +-
resources/js/Components/ApplicationLogo.jsx | 11 +
resources/js/Components/Checkbox.jsx | 12 +
resources/js/Components/DangerButton.jsx | 20 +
resources/js/Components/Dropdown.jsx | 107 ++++++
resources/js/Components/InputError.jsx | 10 +
resources/js/Components/InputLabel.jsx | 18 +
resources/js/Components/Modal.jsx | 65 ++++
resources/js/Components/NavLink.jsx | 23 ++
resources/js/Components/PrimaryButton.jsx | 20 +
resources/js/Components/ResponsiveNavLink.jsx | 21 +
resources/js/Components/SecondaryButton.jsx | 22 ++
resources/js/Components/TextInput.jsx | 30 ++
resources/js/Layouts/AuthenticatedLayout.jsx | 176 +++++++++
resources/js/Layouts/GuestLayout.jsx | 18 +
resources/js/Pages/Auth/ConfirmPassword.jsx | 55 +++
resources/js/Pages/Auth/ForgotPassword.jsx | 55 +++
resources/js/Pages/Auth/Login.jsx | 100 +++++
resources/js/Pages/Auth/Register.jsx | 120 ++++++
resources/js/Pages/Auth/ResetPassword.jsx | 94 +++++
resources/js/Pages/Auth/VerifyEmail.jsx | 50 +++
resources/js/Pages/Dashboard.jsx | 26 ++
resources/js/Pages/Profile/Edit.jsx | 39 ++
.../Pages/Profile/Partials/DeleteUserForm.jsx | 120 ++++++
.../Profile/Partials/UpdatePasswordForm.jsx | 142 +++++++
.../Partials/UpdateProfileInformationForm.jsx | 113 ++++++
resources/js/Pages/Welcome.jsx | 361 ++++++++++++++++++
resources/js/app.js | 1 -
resources/js/app.jsx | 25 ++
resources/views/app.blade.php | 22 ++
resources/views/welcome.blade.php | 277 --------------
routes/auth.php | 59 +++
routes/web.php | 22 +-
tailwind.config.js | 22 ++
tests/Feature/Auth/AuthenticationTest.php | 54 +++
tests/Feature/Auth/EmailVerificationTest.php | 58 +++
.../Feature/Auth/PasswordConfirmationTest.php | 44 +++
tests/Feature/Auth/PasswordResetTest.php | 73 ++++
tests/Feature/Auth/PasswordUpdateTest.php | 51 +++
tests/Feature/Auth/RegistrationTest.php | 31 ++
tests/Feature/ProfileTest.php | 99 +++++
vite.config.js | 6 +-
62 files changed, 3279 insertions(+), 297 deletions(-)
create mode 100644 app/Http/Controllers/Auth/AuthenticatedSessionController.php
create mode 100644 app/Http/Controllers/Auth/ConfirmablePasswordController.php
create mode 100644 app/Http/Controllers/Auth/EmailVerificationNotificationController.php
create mode 100644 app/Http/Controllers/Auth/EmailVerificationPromptController.php
create mode 100644 app/Http/Controllers/Auth/NewPasswordController.php
create mode 100644 app/Http/Controllers/Auth/PasswordController.php
create mode 100644 app/Http/Controllers/Auth/PasswordResetLinkController.php
create mode 100644 app/Http/Controllers/Auth/RegisteredUserController.php
create mode 100644 app/Http/Controllers/Auth/VerifyEmailController.php
create mode 100644 app/Http/Controllers/ProfileController.php
create mode 100644 app/Http/Middleware/HandleInertiaRequests.php
create mode 100644 app/Http/Requests/Auth/LoginRequest.php
create mode 100644 app/Http/Requests/ProfileUpdateRequest.php
create mode 100644 jsconfig.json
create mode 100644 postcss.config.js
create mode 100644 resources/js/Components/ApplicationLogo.jsx
create mode 100644 resources/js/Components/Checkbox.jsx
create mode 100644 resources/js/Components/DangerButton.jsx
create mode 100644 resources/js/Components/Dropdown.jsx
create mode 100644 resources/js/Components/InputError.jsx
create mode 100644 resources/js/Components/InputLabel.jsx
create mode 100644 resources/js/Components/Modal.jsx
create mode 100644 resources/js/Components/NavLink.jsx
create mode 100644 resources/js/Components/PrimaryButton.jsx
create mode 100644 resources/js/Components/ResponsiveNavLink.jsx
create mode 100644 resources/js/Components/SecondaryButton.jsx
create mode 100644 resources/js/Components/TextInput.jsx
create mode 100644 resources/js/Layouts/AuthenticatedLayout.jsx
create mode 100644 resources/js/Layouts/GuestLayout.jsx
create mode 100644 resources/js/Pages/Auth/ConfirmPassword.jsx
create mode 100644 resources/js/Pages/Auth/ForgotPassword.jsx
create mode 100644 resources/js/Pages/Auth/Login.jsx
create mode 100644 resources/js/Pages/Auth/Register.jsx
create mode 100644 resources/js/Pages/Auth/ResetPassword.jsx
create mode 100644 resources/js/Pages/Auth/VerifyEmail.jsx
create mode 100644 resources/js/Pages/Dashboard.jsx
create mode 100644 resources/js/Pages/Profile/Edit.jsx
create mode 100644 resources/js/Pages/Profile/Partials/DeleteUserForm.jsx
create mode 100644 resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx
create mode 100644 resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx
create mode 100644 resources/js/Pages/Welcome.jsx
delete mode 100644 resources/js/app.js
create mode 100644 resources/js/app.jsx
create mode 100644 resources/views/app.blade.php
delete mode 100644 resources/views/welcome.blade.php
create mode 100644 routes/auth.php
create mode 100644 tailwind.config.js
create mode 100644 tests/Feature/Auth/AuthenticationTest.php
create mode 100644 tests/Feature/Auth/EmailVerificationTest.php
create mode 100644 tests/Feature/Auth/PasswordConfirmationTest.php
create mode 100644 tests/Feature/Auth/PasswordResetTest.php
create mode 100644 tests/Feature/Auth/PasswordUpdateTest.php
create mode 100644 tests/Feature/Auth/RegistrationTest.php
create mode 100644 tests/Feature/ProfileTest.php
diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
new file mode 100644
index 0000000..d44fe97
--- /dev/null
+++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
@@ -0,0 +1,52 @@
+ Route::has('password.request'),
+ 'status' => session('status'),
+ ]);
+ }
+
+ /**
+ * Handle an incoming authentication request.
+ */
+ public function store(LoginRequest $request): RedirectResponse
+ {
+ $request->authenticate();
+
+ $request->session()->regenerate();
+
+ return redirect()->intended(route('dashboard', absolute: false));
+ }
+
+ /**
+ * Destroy an authenticated session.
+ */
+ public function destroy(Request $request): RedirectResponse
+ {
+ Auth::guard('web')->logout();
+
+ $request->session()->invalidate();
+
+ $request->session()->regenerateToken();
+
+ return redirect('/');
+ }
+}
diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php
new file mode 100644
index 0000000..d2b1f14
--- /dev/null
+++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php
@@ -0,0 +1,41 @@
+validate([
+ 'email' => $request->user()->email,
+ 'password' => $request->password,
+ ])) {
+ throw ValidationException::withMessages([
+ 'password' => __('auth.password'),
+ ]);
+ }
+
+ $request->session()->put('auth.password_confirmed_at', time());
+
+ return redirect()->intended(route('dashboard', absolute: false));
+ }
+}
diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php
new file mode 100644
index 0000000..f64fa9b
--- /dev/null
+++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php
@@ -0,0 +1,24 @@
+user()->hasVerifiedEmail()) {
+ return redirect()->intended(route('dashboard', absolute: false));
+ }
+
+ $request->user()->sendEmailVerificationNotification();
+
+ return back()->with('status', 'verification-link-sent');
+ }
+}
diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php
new file mode 100644
index 0000000..b42e0d5
--- /dev/null
+++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php
@@ -0,0 +1,22 @@
+user()->hasVerifiedEmail()
+ ? redirect()->intended(route('dashboard', absolute: false))
+ : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
+ }
+}
diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php
new file mode 100644
index 0000000..394cc4a
--- /dev/null
+++ b/app/Http/Controllers/Auth/NewPasswordController.php
@@ -0,0 +1,69 @@
+ $request->email,
+ 'token' => $request->route('token'),
+ ]);
+ }
+
+ /**
+ * Handle an incoming new password request.
+ *
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function store(Request $request): RedirectResponse
+ {
+ $request->validate([
+ 'token' => 'required',
+ 'email' => 'required|email',
+ 'password' => ['required', 'confirmed', Rules\Password::defaults()],
+ ]);
+
+ // Here we will attempt to reset the user's password. If it is successful we
+ // will update the password on an actual user model and persist it to the
+ // database. Otherwise we will parse the error and return the response.
+ $status = Password::reset(
+ $request->only('email', 'password', 'password_confirmation', 'token'),
+ function ($user) use ($request) {
+ $user->forceFill([
+ 'password' => Hash::make($request->password),
+ 'remember_token' => Str::random(60),
+ ])->save();
+
+ event(new PasswordReset($user));
+ }
+ );
+
+ // If the password was successfully reset, we will redirect the user back to
+ // the application's home authenticated view. If there is an error we can
+ // redirect them back to where they came from with their error message.
+ if ($status == Password::PASSWORD_RESET) {
+ return redirect()->route('login')->with('status', __($status));
+ }
+
+ throw ValidationException::withMessages([
+ 'email' => [trans($status)],
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php
new file mode 100644
index 0000000..57a82b5
--- /dev/null
+++ b/app/Http/Controllers/Auth/PasswordController.php
@@ -0,0 +1,29 @@
+validate([
+ 'current_password' => ['required', 'current_password'],
+ 'password' => ['required', Password::defaults(), 'confirmed'],
+ ]);
+
+ $request->user()->update([
+ 'password' => Hash::make($validated['password']),
+ ]);
+
+ return back();
+ }
+}
diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php
new file mode 100644
index 0000000..b22c544
--- /dev/null
+++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php
@@ -0,0 +1,51 @@
+ session('status'),
+ ]);
+ }
+
+ /**
+ * Handle an incoming password reset link request.
+ *
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function store(Request $request): RedirectResponse
+ {
+ $request->validate([
+ 'email' => 'required|email',
+ ]);
+
+ // We will send the password reset link to this user. Once we have attempted
+ // to send the link, we will examine the response then see the message we
+ // need to show to the user. Finally, we'll send out a proper response.
+ $status = Password::sendResetLink(
+ $request->only('email')
+ );
+
+ if ($status == Password::RESET_LINK_SENT) {
+ return back()->with('status', __($status));
+ }
+
+ throw ValidationException::withMessages([
+ 'email' => [trans($status)],
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php
new file mode 100644
index 0000000..53a546b
--- /dev/null
+++ b/app/Http/Controllers/Auth/RegisteredUserController.php
@@ -0,0 +1,51 @@
+validate([
+ 'name' => 'required|string|max:255',
+ 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
+ 'password' => ['required', 'confirmed', Rules\Password::defaults()],
+ ]);
+
+ $user = User::create([
+ 'name' => $request->name,
+ 'email' => $request->email,
+ 'password' => Hash::make($request->password),
+ ]);
+
+ event(new Registered($user));
+
+ Auth::login($user);
+
+ return redirect(route('dashboard', absolute: false));
+ }
+}
diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php
new file mode 100644
index 0000000..784765e
--- /dev/null
+++ b/app/Http/Controllers/Auth/VerifyEmailController.php
@@ -0,0 +1,27 @@
+user()->hasVerifiedEmail()) {
+ return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
+ }
+
+ if ($request->user()->markEmailAsVerified()) {
+ event(new Verified($request->user()));
+ }
+
+ return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
+ }
+}
diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php
new file mode 100644
index 0000000..873b4f7
--- /dev/null
+++ b/app/Http/Controllers/ProfileController.php
@@ -0,0 +1,63 @@
+ $request->user() instanceof MustVerifyEmail,
+ 'status' => session('status'),
+ ]);
+ }
+
+ /**
+ * Update the user's profile information.
+ */
+ public function update(ProfileUpdateRequest $request): RedirectResponse
+ {
+ $request->user()->fill($request->validated());
+
+ if ($request->user()->isDirty('email')) {
+ $request->user()->email_verified_at = null;
+ }
+
+ $request->user()->save();
+
+ return Redirect::route('profile.edit');
+ }
+
+ /**
+ * Delete the user's account.
+ */
+ public function destroy(Request $request): RedirectResponse
+ {
+ $request->validate([
+ 'password' => ['required', 'current_password'],
+ ]);
+
+ $user = $request->user();
+
+ Auth::logout();
+
+ $user->delete();
+
+ $request->session()->invalidate();
+ $request->session()->regenerateToken();
+
+ return Redirect::to('/');
+ }
+}
diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php
new file mode 100644
index 0000000..3867f22
--- /dev/null
+++ b/app/Http/Middleware/HandleInertiaRequests.php
@@ -0,0 +1,39 @@
+
+ */
+ public function share(Request $request): array
+ {
+ return [
+ ...parent::share($request),
+ 'auth' => [
+ 'user' => $request->user(),
+ ],
+ ];
+ }
+}
diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php
new file mode 100644
index 0000000..2574642
--- /dev/null
+++ b/app/Http/Requests/Auth/LoginRequest.php
@@ -0,0 +1,85 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'email' => ['required', 'string', 'email'],
+ 'password' => ['required', 'string'],
+ ];
+ }
+
+ /**
+ * Attempt to authenticate the request's credentials.
+ *
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function authenticate(): void
+ {
+ $this->ensureIsNotRateLimited();
+
+ if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
+ RateLimiter::hit($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.failed'),
+ ]);
+ }
+
+ RateLimiter::clear($this->throttleKey());
+ }
+
+ /**
+ * Ensure the login request is not rate limited.
+ *
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function ensureIsNotRateLimited(): void
+ {
+ if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
+ return;
+ }
+
+ event(new Lockout($this));
+
+ $seconds = RateLimiter::availableIn($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.throttle', [
+ 'seconds' => $seconds,
+ 'minutes' => ceil($seconds / 60),
+ ]),
+ ]);
+ }
+
+ /**
+ * Get the rate limiting throttle key for the request.
+ */
+ public function throttleKey(): string
+ {
+ return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
+ }
+}
diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php
new file mode 100644
index 0000000..3622a8f
--- /dev/null
+++ b/app/Http/Requests/ProfileUpdateRequest.php
@@ -0,0 +1,30 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => [
+ 'required',
+ 'string',
+ 'lowercase',
+ 'email',
+ 'max:255',
+ Rule::unique(User::class)->ignore($this->user()->id),
+ ],
+ ];
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 452e6b6..96e9f6c 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -2,6 +2,7 @@
namespace App\Providers;
+use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -19,6 +20,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
- //
+ Vite::prefetch(concurrency: 3);
}
}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index c183276..5c02a59 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -11,6 +11,11 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
+ $middleware->web(append: [
+ \App\Http\Middleware\HandleInertiaRequests::class,
+ \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
+ ]);
+
//
})
->withExceptions(function (Exceptions $exceptions): void {
diff --git a/composer.json b/composer.json
index 043298e..3398c73 100644
--- a/composer.json
+++ b/composer.json
@@ -8,11 +8,15 @@
"require": {
"php": "^8.2",
"filament/filament": "^4.0",
+ "inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
- "laravel/tinker": "^2.10.1"
+ "laravel/sanctum": "^4.0",
+ "laravel/tinker": "^2.10.1",
+ "tightenco/ziggy": "^2.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
+ "laravel/breeze": "^2.3",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
diff --git a/composer.lock b/composer.lock
index a8933bc..bf47c37 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d06fce5a31d9da3c576b967652ed06de",
+ "content-hash": "4c4fa1ad6ac32d349ed330af73603676",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -1973,6 +1973,76 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
+ {
+ "name": "inertiajs/inertia-laravel",
+ "version": "v2.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/inertiajs/inertia-laravel.git",
+ "reference": "e2ab960c6e02cb374f0ec89b5681df144bb256e9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/e2ab960c6e02cb374f0ec89b5681df144bb256e9",
+ "reference": "e2ab960c6e02cb374f0ec89b5681df144bb256e9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": "^10.0|^11.0|^12.0",
+ "php": "^8.1.0",
+ "symfony/console": "^6.2|^7.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.2",
+ "larastan/larastan": "^3.0",
+ "laravel/pint": "^1.16",
+ "mockery/mockery": "^1.3.3",
+ "orchestra/testbench": "^8.0|^9.2|^10.0",
+ "phpunit/phpunit": "^10.4|^11.5",
+ "roave/security-advisories": "dev-master"
+ },
+ "suggest": {
+ "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Inertia\\ServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "./helpers.php"
+ ],
+ "psr-4": {
+ "Inertia\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jonathan Reinink",
+ "email": "jonathan@reinink.ca",
+ "homepage": "https://reinink.ca"
+ }
+ ],
+ "description": "The Laravel adapter for Inertia.js.",
+ "keywords": [
+ "inertia",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/inertiajs/inertia-laravel/issues",
+ "source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.8"
+ },
+ "time": "2025-09-26T15:12:11+00:00"
+ },
{
"name": "kirschbaum-development/eloquent-power-joins",
"version": "4.2.8",
@@ -2315,6 +2385,70 @@
},
"time": "2025-09-19T13:47:56+00:00"
},
+ {
+ "name": "laravel/sanctum",
+ "version": "v4.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/sanctum.git",
+ "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
+ "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "illuminate/console": "^11.0|^12.0",
+ "illuminate/contracts": "^11.0|^12.0",
+ "illuminate/database": "^11.0|^12.0",
+ "illuminate/support": "^11.0|^12.0",
+ "php": "^8.2",
+ "symfony/console": "^7.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^9.0|^10.0",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Sanctum\\SanctumServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Sanctum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
+ "keywords": [
+ "auth",
+ "laravel",
+ "sanctum"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/sanctum/issues",
+ "source": "https://github.com/laravel/sanctum"
+ },
+ "time": "2025-07-09T19:45:24+00:00"
+ },
{
"name": "laravel/serializable-closure",
"version": "v2.0.5",
@@ -7892,6 +8026,76 @@
],
"time": "2025-08-13T11:49:31+00:00"
},
+ {
+ "name": "tightenco/ziggy",
+ "version": "v2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tighten/ziggy.git",
+ "reference": "cccc6035c109daab03a33926b3a8499bedbed01f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tighten/ziggy/zipball/cccc6035c109daab03a33926b3a8499bedbed01f",
+ "reference": "cccc6035c109daab03a33926b3a8499bedbed01f",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": ">=9.0",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "laravel/folio": "^1.1",
+ "orchestra/testbench": "^7.0 || ^8.0 || ^9.0 || ^10.0",
+ "pestphp/pest": "^2.26|^3.0",
+ "pestphp/pest-plugin-laravel": "^2.4|^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Tighten\\Ziggy\\ZiggyServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Tighten\\Ziggy\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Daniel Coulbourne",
+ "email": "daniel@tighten.co"
+ },
+ {
+ "name": "Jake Bathman",
+ "email": "jake@tighten.co"
+ },
+ {
+ "name": "Jacob Baker-Kretzmar",
+ "email": "jacob@tighten.co"
+ }
+ ],
+ "description": "Use your Laravel named routes in JavaScript.",
+ "homepage": "https://github.com/tighten/ziggy",
+ "keywords": [
+ "Ziggy",
+ "javascript",
+ "laravel",
+ "routes"
+ ],
+ "support": {
+ "issues": "https://github.com/tighten/ziggy/issues",
+ "source": "https://github.com/tighten/ziggy/tree/v2.6.0"
+ },
+ "time": "2025-09-15T00:00:26+00:00"
+ },
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.3.0",
@@ -8419,6 +8623,67 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
+ {
+ "name": "laravel/breeze",
+ "version": "v2.3.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/breeze.git",
+ "reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/breeze/zipball/1a29c5792818bd4cddf70b5f743a227e02fbcfcd",
+ "reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^11.0|^12.0",
+ "illuminate/filesystem": "^11.0|^12.0",
+ "illuminate/support": "^11.0|^12.0",
+ "illuminate/validation": "^11.0|^12.0",
+ "php": "^8.2.0",
+ "symfony/console": "^7.0"
+ },
+ "require-dev": {
+ "laravel/framework": "^11.0|^12.0",
+ "orchestra/testbench-core": "^9.0|^10.0",
+ "phpstan/phpstan": "^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Breeze\\BreezeServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Breeze\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.",
+ "keywords": [
+ "auth",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/breeze/issues",
+ "source": "https://github.com/laravel/breeze"
+ },
+ "time": "2025-07-18T18:49:59+00:00"
+ },
{
"name": "laravel/pail",
"version": "v1.2.3",
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..6269354
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["resources/js/*"],
+ "ziggy-js": ["./vendor/tightenco/ziggy"]
+ }
+ },
+ "exclude": ["node_modules", "public"]
+}
diff --git a/package.json b/package.json
index a5707d8..3f8950f 100644
--- a/package.json
+++ b/package.json
@@ -7,11 +7,19 @@
"dev": "vite"
},
"devDependencies": {
+ "@headlessui/react": "^2.0.0",
+ "@inertiajs/react": "^2.0.0",
+ "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/vite": "^4.0.0",
+ "@vitejs/plugin-react": "^4.2.0",
+ "autoprefixer": "^10.4.12",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
- "tailwindcss": "^4.0.0",
+ "postcss": "^8.4.31",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tailwindcss": "^3.2.1",
"vite": "^7.0.4"
}
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..49c0612
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/resources/css/app.css b/resources/css/app.css
index 3e6abea..b5c61c9 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -1,11 +1,3 @@
-@import 'tailwindcss';
-
-@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
-@source '../../storage/framework/views/*.php';
-@source '../**/*.blade.php';
-@source '../**/*.js';
-
-@theme {
- --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
- 'Segoe UI Symbol', 'Noto Color Emoji';
-}
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/resources/js/Components/ApplicationLogo.jsx b/resources/js/Components/ApplicationLogo.jsx
new file mode 100644
index 0000000..160fb62
--- /dev/null
+++ b/resources/js/Components/ApplicationLogo.jsx
@@ -0,0 +1,11 @@
+export default function ApplicationLogo(props) {
+ return (
+
+
+
+ );
+}
diff --git a/resources/js/Components/Checkbox.jsx b/resources/js/Components/Checkbox.jsx
new file mode 100644
index 0000000..34a31fe
--- /dev/null
+++ b/resources/js/Components/Checkbox.jsx
@@ -0,0 +1,12 @@
+export default function Checkbox({ className = '', ...props }) {
+ return (
+
+ );
+}
diff --git a/resources/js/Components/DangerButton.jsx b/resources/js/Components/DangerButton.jsx
new file mode 100644
index 0000000..482d6eb
--- /dev/null
+++ b/resources/js/Components/DangerButton.jsx
@@ -0,0 +1,20 @@
+export default function DangerButton({
+ className = '',
+ disabled,
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/resources/js/Components/Dropdown.jsx b/resources/js/Components/Dropdown.jsx
new file mode 100644
index 0000000..5f037ef
--- /dev/null
+++ b/resources/js/Components/Dropdown.jsx
@@ -0,0 +1,107 @@
+import { Transition } from '@headlessui/react';
+import { Link } from '@inertiajs/react';
+import { createContext, useContext, useState } from 'react';
+
+const DropDownContext = createContext();
+
+const Dropdown = ({ children }) => {
+ const [open, setOpen] = useState(false);
+
+ const toggleOpen = () => {
+ setOpen((previousState) => !previousState);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+const Trigger = ({ children }) => {
+ const { open, setOpen, toggleOpen } = useContext(DropDownContext);
+
+ return (
+ <>
+ {children}
+
+ {open && (
+ setOpen(false)}
+ >
+ )}
+ >
+ );
+};
+
+const Content = ({
+ align = 'right',
+ width = '48',
+ contentClasses = 'py-1 bg-white',
+ children,
+}) => {
+ const { open, setOpen } = useContext(DropDownContext);
+
+ let alignmentClasses = 'origin-top';
+
+ if (align === 'left') {
+ alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0';
+ } else if (align === 'right') {
+ alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0';
+ }
+
+ let widthClasses = '';
+
+ if (width === '48') {
+ widthClasses = 'w-48';
+ }
+
+ return (
+ <>
+
+ setOpen(false)}
+ >
+
+ {children}
+
+
+
+ >
+ );
+};
+
+const DropdownLink = ({ className = '', children, ...props }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+Dropdown.Trigger = Trigger;
+Dropdown.Content = Content;
+Dropdown.Link = DropdownLink;
+
+export default Dropdown;
diff --git a/resources/js/Components/InputError.jsx b/resources/js/Components/InputError.jsx
new file mode 100644
index 0000000..cf8e9e1
--- /dev/null
+++ b/resources/js/Components/InputError.jsx
@@ -0,0 +1,10 @@
+export default function InputError({ message, className = '', ...props }) {
+ return message ? (
+
+ {message}
+
+ ) : null;
+}
diff --git a/resources/js/Components/InputLabel.jsx b/resources/js/Components/InputLabel.jsx
new file mode 100644
index 0000000..9364f9d
--- /dev/null
+++ b/resources/js/Components/InputLabel.jsx
@@ -0,0 +1,18 @@
+export default function InputLabel({
+ value,
+ className = '',
+ children,
+ ...props
+}) {
+ return (
+
+ {value ? value : children}
+
+ );
+}
diff --git a/resources/js/Components/Modal.jsx b/resources/js/Components/Modal.jsx
new file mode 100644
index 0000000..7560cff
--- /dev/null
+++ b/resources/js/Components/Modal.jsx
@@ -0,0 +1,65 @@
+import {
+ Dialog,
+ DialogPanel,
+ Transition,
+ TransitionChild,
+} from '@headlessui/react';
+
+export default function Modal({
+ children,
+ show = false,
+ maxWidth = '2xl',
+ closeable = true,
+ onClose = () => {},
+}) {
+ const close = () => {
+ if (closeable) {
+ onClose();
+ }
+ };
+
+ const maxWidthClass = {
+ sm: 'sm:max-w-sm',
+ md: 'sm:max-w-md',
+ lg: 'sm:max-w-lg',
+ xl: 'sm:max-w-xl',
+ '2xl': 'sm:max-w-2xl',
+ }[maxWidth];
+
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/resources/js/Components/NavLink.jsx b/resources/js/Components/NavLink.jsx
new file mode 100644
index 0000000..ffe8b02
--- /dev/null
+++ b/resources/js/Components/NavLink.jsx
@@ -0,0 +1,23 @@
+import { Link } from '@inertiajs/react';
+
+export default function NavLink({
+ active = false,
+ className = '',
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/resources/js/Components/PrimaryButton.jsx b/resources/js/Components/PrimaryButton.jsx
new file mode 100644
index 0000000..61e0bca
--- /dev/null
+++ b/resources/js/Components/PrimaryButton.jsx
@@ -0,0 +1,20 @@
+export default function PrimaryButton({
+ className = '',
+ disabled,
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/resources/js/Components/ResponsiveNavLink.jsx b/resources/js/Components/ResponsiveNavLink.jsx
new file mode 100644
index 0000000..47e0bca
--- /dev/null
+++ b/resources/js/Components/ResponsiveNavLink.jsx
@@ -0,0 +1,21 @@
+import { Link } from '@inertiajs/react';
+
+export default function ResponsiveNavLink({
+ active = false,
+ className = '',
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/resources/js/Components/SecondaryButton.jsx b/resources/js/Components/SecondaryButton.jsx
new file mode 100644
index 0000000..ce8b42d
--- /dev/null
+++ b/resources/js/Components/SecondaryButton.jsx
@@ -0,0 +1,22 @@
+export default function SecondaryButton({
+ type = 'button',
+ className = '',
+ disabled,
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/resources/js/Components/TextInput.jsx b/resources/js/Components/TextInput.jsx
new file mode 100644
index 0000000..6d9c3b7
--- /dev/null
+++ b/resources/js/Components/TextInput.jsx
@@ -0,0 +1,30 @@
+import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
+
+export default forwardRef(function TextInput(
+ { type = 'text', className = '', isFocused = false, ...props },
+ ref,
+) {
+ const localRef = useRef(null);
+
+ useImperativeHandle(ref, () => ({
+ focus: () => localRef.current?.focus(),
+ }));
+
+ useEffect(() => {
+ if (isFocused) {
+ localRef.current?.focus();
+ }
+ }, [isFocused]);
+
+ return (
+
+ );
+});
diff --git a/resources/js/Layouts/AuthenticatedLayout.jsx b/resources/js/Layouts/AuthenticatedLayout.jsx
new file mode 100644
index 0000000..db7e353
--- /dev/null
+++ b/resources/js/Layouts/AuthenticatedLayout.jsx
@@ -0,0 +1,176 @@
+import ApplicationLogo from '@/Components/ApplicationLogo';
+import Dropdown from '@/Components/Dropdown';
+import NavLink from '@/Components/NavLink';
+import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
+import { Link, usePage } from '@inertiajs/react';
+import { useState } from 'react';
+
+export default function AuthenticatedLayout({ header, children }) {
+ const user = usePage().props.auth.user;
+
+ const [showingNavigationDropdown, setShowingNavigationDropdown] =
+ useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
+
+ {user.name}
+
+
+
+
+
+
+
+
+
+
+ Profile
+
+
+ Log Out
+
+
+
+
+
+
+
+
+ setShowingNavigationDropdown(
+ (previousState) => !previousState,
+ )
+ }
+ className="inline-flex items-center justify-center rounded-md p-2 text-gray-400 transition duration-150 ease-in-out hover:bg-gray-100 hover:text-gray-500 focus:bg-gray-100 focus:text-gray-500 focus:outline-none"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+ {user.name}
+
+
+ {user.email}
+
+
+
+
+
+ Profile
+
+
+ Log Out
+
+
+
+
+
+
+ {header && (
+
+ )}
+
+
{children}
+
+ );
+}
diff --git a/resources/js/Layouts/GuestLayout.jsx b/resources/js/Layouts/GuestLayout.jsx
new file mode 100644
index 0000000..aaf6135
--- /dev/null
+++ b/resources/js/Layouts/GuestLayout.jsx
@@ -0,0 +1,18 @@
+import ApplicationLogo from '@/Components/ApplicationLogo';
+import { Link } from '@inertiajs/react';
+
+export default function GuestLayout({ children }) {
+ return (
+
+ );
+}
diff --git a/resources/js/Pages/Auth/ConfirmPassword.jsx b/resources/js/Pages/Auth/ConfirmPassword.jsx
new file mode 100644
index 0000000..8dc63ce
--- /dev/null
+++ b/resources/js/Pages/Auth/ConfirmPassword.jsx
@@ -0,0 +1,55 @@
+import InputError from '@/Components/InputError';
+import InputLabel from '@/Components/InputLabel';
+import PrimaryButton from '@/Components/PrimaryButton';
+import TextInput from '@/Components/TextInput';
+import GuestLayout from '@/Layouts/GuestLayout';
+import { Head, useForm } from '@inertiajs/react';
+
+export default function ConfirmPassword() {
+ const { data, setData, post, processing, errors, reset } = useForm({
+ password: '',
+ });
+
+ const submit = (e) => {
+ e.preventDefault();
+
+ post(route('password.confirm'), {
+ onFinish: () => reset('password'),
+ });
+ };
+
+ return (
+
+
+
+
+ This is a secure area of the application. Please confirm your
+ password before continuing.
+
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Auth/ForgotPassword.jsx b/resources/js/Pages/Auth/ForgotPassword.jsx
new file mode 100644
index 0000000..ab27b28
--- /dev/null
+++ b/resources/js/Pages/Auth/ForgotPassword.jsx
@@ -0,0 +1,55 @@
+import InputError from '@/Components/InputError';
+import PrimaryButton from '@/Components/PrimaryButton';
+import TextInput from '@/Components/TextInput';
+import GuestLayout from '@/Layouts/GuestLayout';
+import { Head, useForm } from '@inertiajs/react';
+
+export default function ForgotPassword({ status }) {
+ const { data, setData, post, processing, errors } = useForm({
+ email: '',
+ });
+
+ const submit = (e) => {
+ e.preventDefault();
+
+ post(route('password.email'));
+ };
+
+ return (
+
+
+
+
+ Forgot your password? No problem. Just let us know your email
+ address and we will email you a password reset link that will
+ allow you to choose a new one.
+
+
+ {status && (
+
+ {status}
+
+ )}
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Auth/Login.jsx b/resources/js/Pages/Auth/Login.jsx
new file mode 100644
index 0000000..18a1d45
--- /dev/null
+++ b/resources/js/Pages/Auth/Login.jsx
@@ -0,0 +1,100 @@
+import Checkbox from '@/Components/Checkbox';
+import InputError from '@/Components/InputError';
+import InputLabel from '@/Components/InputLabel';
+import PrimaryButton from '@/Components/PrimaryButton';
+import TextInput from '@/Components/TextInput';
+import GuestLayout from '@/Layouts/GuestLayout';
+import { Head, Link, useForm } from '@inertiajs/react';
+
+export default function Login({ status, canResetPassword }) {
+ const { data, setData, post, processing, errors, reset } = useForm({
+ email: '',
+ password: '',
+ remember: false,
+ });
+
+ const submit = (e) => {
+ e.preventDefault();
+
+ post(route('login'), {
+ onFinish: () => reset('password'),
+ });
+ };
+
+ return (
+
+
+
+ {status && (
+
+ {status}
+
+ )}
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Auth/Register.jsx b/resources/js/Pages/Auth/Register.jsx
new file mode 100644
index 0000000..4cd27d6
--- /dev/null
+++ b/resources/js/Pages/Auth/Register.jsx
@@ -0,0 +1,120 @@
+import InputError from '@/Components/InputError';
+import InputLabel from '@/Components/InputLabel';
+import PrimaryButton from '@/Components/PrimaryButton';
+import TextInput from '@/Components/TextInput';
+import GuestLayout from '@/Layouts/GuestLayout';
+import { Head, Link, useForm } from '@inertiajs/react';
+
+export default function Register() {
+ const { data, setData, post, processing, errors, reset } = useForm({
+ name: '',
+ email: '',
+ password: '',
+ password_confirmation: '',
+ });
+
+ const submit = (e) => {
+ e.preventDefault();
+
+ post(route('register'), {
+ onFinish: () => reset('password', 'password_confirmation'),
+ });
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Auth/ResetPassword.jsx b/resources/js/Pages/Auth/ResetPassword.jsx
new file mode 100644
index 0000000..c2a7a03
--- /dev/null
+++ b/resources/js/Pages/Auth/ResetPassword.jsx
@@ -0,0 +1,94 @@
+import InputError from '@/Components/InputError';
+import InputLabel from '@/Components/InputLabel';
+import PrimaryButton from '@/Components/PrimaryButton';
+import TextInput from '@/Components/TextInput';
+import GuestLayout from '@/Layouts/GuestLayout';
+import { Head, useForm } from '@inertiajs/react';
+
+export default function ResetPassword({ token, email }) {
+ const { data, setData, post, processing, errors, reset } = useForm({
+ token: token,
+ email: email,
+ password: '',
+ password_confirmation: '',
+ });
+
+ const submit = (e) => {
+ e.preventDefault();
+
+ post(route('password.store'), {
+ onFinish: () => reset('password', 'password_confirmation'),
+ });
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Auth/VerifyEmail.jsx b/resources/js/Pages/Auth/VerifyEmail.jsx
new file mode 100644
index 0000000..e4a97cb
--- /dev/null
+++ b/resources/js/Pages/Auth/VerifyEmail.jsx
@@ -0,0 +1,50 @@
+import PrimaryButton from '@/Components/PrimaryButton';
+import GuestLayout from '@/Layouts/GuestLayout';
+import { Head, Link, useForm } from '@inertiajs/react';
+
+export default function VerifyEmail({ status }) {
+ const { post, processing } = useForm({});
+
+ const submit = (e) => {
+ e.preventDefault();
+
+ post(route('verification.send'));
+ };
+
+ return (
+
+
+
+
+ Thanks for signing up! Before getting started, could you verify
+ your email address by clicking on the link we just emailed to
+ you? If you didn't receive the email, we will gladly send you
+ another.
+
+
+ {status === 'verification-link-sent' && (
+
+ A new verification link has been sent to the email address
+ you provided during registration.
+
+ )}
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Dashboard.jsx b/resources/js/Pages/Dashboard.jsx
new file mode 100644
index 0000000..8f50580
--- /dev/null
+++ b/resources/js/Pages/Dashboard.jsx
@@ -0,0 +1,26 @@
+import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
+import { Head } from '@inertiajs/react';
+
+export default function Dashboard() {
+ return (
+
+ Dashboard
+
+ }
+ >
+
+
+
+
+
+
+ You're logged in!
+
+
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Profile/Edit.jsx b/resources/js/Pages/Profile/Edit.jsx
new file mode 100644
index 0000000..b3a1a9f
--- /dev/null
+++ b/resources/js/Pages/Profile/Edit.jsx
@@ -0,0 +1,39 @@
+import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
+import { Head } from '@inertiajs/react';
+import DeleteUserForm from './Partials/DeleteUserForm';
+import UpdatePasswordForm from './Partials/UpdatePasswordForm';
+import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
+
+export default function Edit({ mustVerifyEmail, status }) {
+ return (
+
+ Profile
+
+ }
+ >
+
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx b/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx
new file mode 100644
index 0000000..350f9da
--- /dev/null
+++ b/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx
@@ -0,0 +1,120 @@
+import DangerButton from '@/Components/DangerButton';
+import InputError from '@/Components/InputError';
+import InputLabel from '@/Components/InputLabel';
+import Modal from '@/Components/Modal';
+import SecondaryButton from '@/Components/SecondaryButton';
+import TextInput from '@/Components/TextInput';
+import { useForm } from '@inertiajs/react';
+import { useRef, useState } from 'react';
+
+export default function DeleteUserForm({ className = '' }) {
+ const [confirmingUserDeletion, setConfirmingUserDeletion] = useState(false);
+ const passwordInput = useRef();
+
+ const {
+ data,
+ setData,
+ delete: destroy,
+ processing,
+ reset,
+ errors,
+ clearErrors,
+ } = useForm({
+ password: '',
+ });
+
+ const confirmUserDeletion = () => {
+ setConfirmingUserDeletion(true);
+ };
+
+ const deleteUser = (e) => {
+ e.preventDefault();
+
+ destroy(route('profile.destroy'), {
+ preserveScroll: true,
+ onSuccess: () => closeModal(),
+ onError: () => passwordInput.current.focus(),
+ onFinish: () => reset(),
+ });
+ };
+
+ const closeModal = () => {
+ setConfirmingUserDeletion(false);
+
+ clearErrors();
+ reset();
+ };
+
+ return (
+
+
+
+ Delete Account
+
+
+
+ Once your account is deleted, all of its resources and data
+ will be permanently deleted. Before deleting your account,
+ please download any data or information that you wish to
+ retain.
+
+
+
+
+ Delete Account
+
+
+
+
+
+
+ );
+}
diff --git a/resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx
new file mode 100644
index 0000000..fa2c44a
--- /dev/null
+++ b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx
@@ -0,0 +1,142 @@
+import InputError from '@/Components/InputError';
+import InputLabel from '@/Components/InputLabel';
+import PrimaryButton from '@/Components/PrimaryButton';
+import TextInput from '@/Components/TextInput';
+import { Transition } from '@headlessui/react';
+import { useForm } from '@inertiajs/react';
+import { useRef } from 'react';
+
+export default function UpdatePasswordForm({ className = '' }) {
+ const passwordInput = useRef();
+ const currentPasswordInput = useRef();
+
+ const {
+ data,
+ setData,
+ errors,
+ put,
+ reset,
+ processing,
+ recentlySuccessful,
+ } = useForm({
+ current_password: '',
+ password: '',
+ password_confirmation: '',
+ });
+
+ const updatePassword = (e) => {
+ e.preventDefault();
+
+ put(route('password.update'), {
+ preserveScroll: true,
+ onSuccess: () => reset(),
+ onError: (errors) => {
+ if (errors.password) {
+ reset('password', 'password_confirmation');
+ passwordInput.current.focus();
+ }
+
+ if (errors.current_password) {
+ reset('current_password');
+ currentPasswordInput.current.focus();
+ }
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx
new file mode 100644
index 0000000..d90fd74
--- /dev/null
+++ b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx
@@ -0,0 +1,113 @@
+import InputError from '@/Components/InputError';
+import InputLabel from '@/Components/InputLabel';
+import PrimaryButton from '@/Components/PrimaryButton';
+import TextInput from '@/Components/TextInput';
+import { Transition } from '@headlessui/react';
+import { Link, useForm, usePage } from '@inertiajs/react';
+
+export default function UpdateProfileInformation({
+ mustVerifyEmail,
+ status,
+ className = '',
+}) {
+ const user = usePage().props.auth.user;
+
+ const { data, setData, patch, errors, processing, recentlySuccessful } =
+ useForm({
+ name: user.name,
+ email: user.email,
+ });
+
+ const submit = (e) => {
+ e.preventDefault();
+
+ patch(route('profile.update'));
+ };
+
+ return (
+
+ );
+}
diff --git a/resources/js/Pages/Welcome.jsx b/resources/js/Pages/Welcome.jsx
new file mode 100644
index 0000000..c3abba2
--- /dev/null
+++ b/resources/js/Pages/Welcome.jsx
@@ -0,0 +1,361 @@
+import { Head, Link } from '@inertiajs/react';
+
+export default function Welcome({ auth, laravelVersion, phpVersion }) {
+ const handleImageError = () => {
+ document
+ .getElementById('screenshot-container')
+ ?.classList.add('!hidden');
+ document.getElementById('docs-card')?.classList.add('!row-span-1');
+ document
+ .getElementById('docs-card-content')
+ ?.classList.add('!flex-row');
+ document.getElementById('background')?.classList.add('!hidden');
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {auth.user ? (
+
+ Dashboard
+
+ ) : (
+ <>
+
+ Log in
+
+
+ Register
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Documentation
+
+
+
+ Laravel has wonderful
+ documentation covering every
+ aspect of the framework.
+ Whether you are a newcomer
+ or have prior experience
+ with Laravel, we recommend
+ reading our documentation
+ from beginning to end.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Laracasts
+
+
+
+ Laracasts offers thousands of video
+ tutorials on Laravel, PHP, and
+ JavaScript development. Check them
+ out, see for yourself, and massively
+ level up your development skills in
+ the process.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Laravel News
+
+
+
+ Laravel News is a community driven
+ portal and newsletter aggregating
+ all of the latest and most important
+ news in the Laravel ecosystem,
+ including new package releases and
+ tutorials.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Vibrant Ecosystem
+
+
+
+ Laravel's robust library of
+ first-party tools and libraries,
+ such as{' '}
+
+ Forge
+
+ ,{' '}
+
+ Vapor
+
+ ,{' '}
+
+ Nova
+
+ ,{' '}
+
+ Envoyer
+
+ , and{' '}
+
+ Herd
+ {' '}
+ help you take your projects to the
+ next level. Pair them with powerful
+ open source libraries like{' '}
+
+ Cashier
+
+ ,{' '}
+
+ Dusk
+
+ ,{' '}
+
+ Echo
+
+ ,{' '}
+
+ Horizon
+
+ ,{' '}
+
+ Sanctum
+
+ ,{' '}
+
+ Telescope
+
+ , and more.
+
+
+
+
+
+
+
+ Laravel v{laravelVersion} (PHP v{phpVersion})
+
+
+
+
+ >
+ );
+}
diff --git a/resources/js/app.js b/resources/js/app.js
deleted file mode 100644
index e59d6a0..0000000
--- a/resources/js/app.js
+++ /dev/null
@@ -1 +0,0 @@
-import './bootstrap';
diff --git a/resources/js/app.jsx b/resources/js/app.jsx
new file mode 100644
index 0000000..9f00218
--- /dev/null
+++ b/resources/js/app.jsx
@@ -0,0 +1,25 @@
+import '../css/app.css';
+import './bootstrap';
+
+import { createInertiaApp } from '@inertiajs/react';
+import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
+import { createRoot } from 'react-dom/client';
+
+const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
+
+createInertiaApp({
+ title: (title) => `${title} - ${appName}`,
+ resolve: (name) =>
+ resolvePageComponent(
+ `./Pages/${name}.jsx`,
+ import.meta.glob('./Pages/**/*.jsx'),
+ ),
+ setup({ el, App, props }) {
+ const root = createRoot(el);
+
+ root.render( );
+ },
+ progress: {
+ color: '#4B5563',
+ },
+});
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php
new file mode 100644
index 0000000..856bcf2
--- /dev/null
+++ b/resources/views/app.blade.php
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ {{ config('app.name', 'Laravel') }}
+
+
+
+
+
+
+ @routes
+ @viteReactRefresh
+ @vite(['resources/js/app.jsx', "resources/js/Pages/{$page['component']}.jsx"])
+ @inertiaHead
+
+
+ @inertia
+
+
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php
deleted file mode 100644
index b7355d7..0000000
--- a/resources/views/welcome.blade.php
+++ /dev/null
@@ -1,277 +0,0 @@
-
-
-
-
-
-
- {{ config('app.name', 'Laravel') }}
-
-
-
-
-
-
- @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
- @vite(['resources/css/app.css', 'resources/js/app.js'])
- @else
-
- @endif
-
-
-
- @if (Route::has('login'))
-
- @auth
-
- Dashboard
-
- @else
-
- Log in
-
-
- @if (Route::has('register'))
-
- Register
-
- @endif
- @endauth
-
- @endif
-
-
-
-
-
Let's get started
-
Laravel has an incredibly rich ecosystem. We suggest starting with the following.
-
-
-
-
- {{-- Laravel Logo --}}
-
-
-
-
-
-
-
-
-
-
- {{-- Light Mode 12 SVG --}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{-- Dark Mode 12 SVG --}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @if (Route::has('login'))
-
- @endif
-
-
diff --git a/routes/auth.php b/routes/auth.php
new file mode 100644
index 0000000..3926ecf
--- /dev/null
+++ b/routes/auth.php
@@ -0,0 +1,59 @@
+group(function () {
+ Route::get('register', [RegisteredUserController::class, 'create'])
+ ->name('register');
+
+ Route::post('register', [RegisteredUserController::class, 'store']);
+
+ Route::get('login', [AuthenticatedSessionController::class, 'create'])
+ ->name('login');
+
+ Route::post('login', [AuthenticatedSessionController::class, 'store']);
+
+ Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
+ ->name('password.request');
+
+ Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
+ ->name('password.email');
+
+ Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
+ ->name('password.reset');
+
+ Route::post('reset-password', [NewPasswordController::class, 'store'])
+ ->name('password.store');
+});
+
+Route::middleware('auth')->group(function () {
+ Route::get('verify-email', EmailVerificationPromptController::class)
+ ->name('verification.notice');
+
+ Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
+ ->middleware(['signed', 'throttle:6,1'])
+ ->name('verification.verify');
+
+ Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
+ ->middleware('throttle:6,1')
+ ->name('verification.send');
+
+ Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
+ ->name('password.confirm');
+
+ Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
+
+ Route::put('password', [PasswordController::class, 'update'])->name('password.update');
+
+ Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
+ ->name('logout');
+});
diff --git a/routes/web.php b/routes/web.php
index 86a06c5..067c4f5 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1,7 +1,27 @@
Route::has('login'),
+ 'canRegister' => Route::has('register'),
+ 'laravelVersion' => Application::VERSION,
+ 'phpVersion' => PHP_VERSION,
+ ]);
});
+
+Route::get('/dashboard', function () {
+ return Inertia::render('Dashboard');
+})->middleware(['auth', 'verified'])->name('dashboard');
+
+Route::middleware('auth')->group(function () {
+ 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');
+});
+
+require __DIR__.'/auth.php';
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..19ae3e3
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,22 @@
+import defaultTheme from 'tailwindcss/defaultTheme';
+import forms from '@tailwindcss/forms';
+
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
+ './storage/framework/views/*.php',
+ './resources/views/**/*.blade.php',
+ './resources/js/**/*.jsx',
+ ],
+
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ['Figtree', ...defaultTheme.fontFamily.sans],
+ },
+ },
+ },
+
+ plugins: [forms],
+};
diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php
new file mode 100644
index 0000000..13dcb7c
--- /dev/null
+++ b/tests/Feature/Auth/AuthenticationTest.php
@@ -0,0 +1,54 @@
+get('/login');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_users_can_authenticate_using_the_login_screen(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->post('/login', [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+
+ $this->assertAuthenticated();
+ $response->assertRedirect(route('dashboard', absolute: false));
+ }
+
+ public function test_users_can_not_authenticate_with_invalid_password(): void
+ {
+ $user = User::factory()->create();
+
+ $this->post('/login', [
+ 'email' => $user->email,
+ 'password' => 'wrong-password',
+ ]);
+
+ $this->assertGuest();
+ }
+
+ public function test_users_can_logout(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)->post('/logout');
+
+ $this->assertGuest();
+ $response->assertRedirect('/');
+ }
+}
diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php
new file mode 100644
index 0000000..705570b
--- /dev/null
+++ b/tests/Feature/Auth/EmailVerificationTest.php
@@ -0,0 +1,58 @@
+unverified()->create();
+
+ $response = $this->actingAs($user)->get('/verify-email');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_email_can_be_verified(): void
+ {
+ $user = User::factory()->unverified()->create();
+
+ Event::fake();
+
+ $verificationUrl = URL::temporarySignedRoute(
+ 'verification.verify',
+ now()->addMinutes(60),
+ ['id' => $user->id, 'hash' => sha1($user->email)]
+ );
+
+ $response = $this->actingAs($user)->get($verificationUrl);
+
+ Event::assertDispatched(Verified::class);
+ $this->assertTrue($user->fresh()->hasVerifiedEmail());
+ $response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
+ }
+
+ public function test_email_is_not_verified_with_invalid_hash(): void
+ {
+ $user = User::factory()->unverified()->create();
+
+ $verificationUrl = URL::temporarySignedRoute(
+ 'verification.verify',
+ now()->addMinutes(60),
+ ['id' => $user->id, 'hash' => sha1('wrong-email')]
+ );
+
+ $this->actingAs($user)->get($verificationUrl);
+
+ $this->assertFalse($user->fresh()->hasVerifiedEmail());
+ }
+}
diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php
new file mode 100644
index 0000000..ff85721
--- /dev/null
+++ b/tests/Feature/Auth/PasswordConfirmationTest.php
@@ -0,0 +1,44 @@
+create();
+
+ $response = $this->actingAs($user)->get('/confirm-password');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_password_can_be_confirmed(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)->post('/confirm-password', [
+ 'password' => 'password',
+ ]);
+
+ $response->assertRedirect();
+ $response->assertSessionHasNoErrors();
+ }
+
+ public function test_password_is_not_confirmed_with_invalid_password(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)->post('/confirm-password', [
+ 'password' => 'wrong-password',
+ ]);
+
+ $response->assertSessionHasErrors();
+ }
+}
diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php
new file mode 100644
index 0000000..aa50350
--- /dev/null
+++ b/tests/Feature/Auth/PasswordResetTest.php
@@ -0,0 +1,73 @@
+get('/forgot-password');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_reset_password_link_can_be_requested(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ $this->post('/forgot-password', ['email' => $user->email]);
+
+ Notification::assertSentTo($user, ResetPassword::class);
+ }
+
+ public function test_reset_password_screen_can_be_rendered(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ $this->post('/forgot-password', ['email' => $user->email]);
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
+ $response = $this->get('/reset-password/'.$notification->token);
+
+ $response->assertStatus(200);
+
+ return true;
+ });
+ }
+
+ public function test_password_can_be_reset_with_valid_token(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ $this->post('/forgot-password', ['email' => $user->email]);
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
+ $response = $this->post('/reset-password', [
+ 'token' => $notification->token,
+ 'email' => $user->email,
+ 'password' => 'password',
+ 'password_confirmation' => 'password',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect(route('login'));
+
+ return true;
+ });
+ }
+}
diff --git a/tests/Feature/Auth/PasswordUpdateTest.php b/tests/Feature/Auth/PasswordUpdateTest.php
new file mode 100644
index 0000000..bbf079d
--- /dev/null
+++ b/tests/Feature/Auth/PasswordUpdateTest.php
@@ -0,0 +1,51 @@
+create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from('/profile')
+ ->put('/password', [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/profile');
+
+ $this->assertTrue(Hash::check('new-password', $user->refresh()->password));
+ }
+
+ public function test_correct_password_must_be_provided_to_update_password(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from('/profile')
+ ->put('/password', [
+ 'current_password' => 'wrong-password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response
+ ->assertSessionHasErrors('current_password')
+ ->assertRedirect('/profile');
+ }
+}
diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php
new file mode 100644
index 0000000..1489d0e
--- /dev/null
+++ b/tests/Feature/Auth/RegistrationTest.php
@@ -0,0 +1,31 @@
+get('/register');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_new_users_can_register(): void
+ {
+ $response = $this->post('/register', [
+ 'name' => 'Test User',
+ 'email' => 'test@example.com',
+ 'password' => 'password',
+ 'password_confirmation' => 'password',
+ ]);
+
+ $this->assertAuthenticated();
+ $response->assertRedirect(route('dashboard', absolute: false));
+ }
+}
diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php
new file mode 100644
index 0000000..49886c3
--- /dev/null
+++ b/tests/Feature/ProfileTest.php
@@ -0,0 +1,99 @@
+create();
+
+ $response = $this
+ ->actingAs($user)
+ ->get('/profile');
+
+ $response->assertOk();
+ }
+
+ public function test_profile_information_can_be_updated(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->patch('/profile', [
+ 'name' => 'Test User',
+ 'email' => 'test@example.com',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/profile');
+
+ $user->refresh();
+
+ $this->assertSame('Test User', $user->name);
+ $this->assertSame('test@example.com', $user->email);
+ $this->assertNull($user->email_verified_at);
+ }
+
+ public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->patch('/profile', [
+ 'name' => 'Test User',
+ 'email' => $user->email,
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/profile');
+
+ $this->assertNotNull($user->refresh()->email_verified_at);
+ }
+
+ public function test_user_can_delete_their_account(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->delete('/profile', [
+ 'password' => 'password',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/');
+
+ $this->assertGuest();
+ $this->assertNull($user->fresh());
+ }
+
+ public function test_correct_password_must_be_provided_to_delete_account(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from('/profile')
+ ->delete('/profile', [
+ 'password' => 'wrong-password',
+ ]);
+
+ $response
+ ->assertSessionHasErrors('password')
+ ->assertRedirect('/profile');
+
+ $this->assertNotNull($user->fresh());
+ }
+}
diff --git a/vite.config.js b/vite.config.js
index 29fbfe9..19f2908 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,13 +1,13 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
-import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({
- input: ['resources/css/app.css', 'resources/js/app.js'],
+ input: 'resources/js/app.jsx',
refresh: true,
}),
- tailwindcss(),
+ react(),
],
});