├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── .prettierrc ├── README.md ├── app ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── NewPasswordController.php │ │ │ ├── PasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── VerifyEmailController.php │ │ ├── Controller.php │ │ └── ProfileController.php │ ├── Middleware │ │ └── HandleInertiaRequests.php │ └── Requests │ │ ├── Auth │ │ └── LoginRequest.php │ │ └── ProfileUpdateRequest.php ├── Models │ └── User.php └── Providers │ └── AppServiceProvider.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── components.json ├── composer.json ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── filesystems.php ├── ide-helper.php ├── logging.php ├── mail.php ├── queue.php ├── services.php ├── session.php └── typescript-transformer.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ └── 0001_01_01_000002_create_jobs_table.php └── seeders │ └── DatabaseSeeder.php ├── inertia ├── app.tsx ├── bootstrap.ts ├── components │ ├── application-logo.tsx │ ├── layouts │ │ ├── authenticated-layout.tsx │ │ ├── guest-layout.tsx │ │ ├── mobile-menu.tsx │ │ ├── navigation.tsx │ │ └── user-dropdown.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── navigation-menu.tsx │ │ ├── switch.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── toggle.tsx ├── css │ └── app.css ├── hooks │ └── use-toast.ts ├── lib │ └── utils.ts ├── pages │ ├── auth │ │ ├── confirm-password.tsx │ │ ├── forgot-password.tsx │ │ ├── login.tsx │ │ ├── register.tsx │ │ ├── reset-password.tsx │ │ └── verify-email.tsx │ ├── dashboard.tsx │ ├── profile │ │ ├── edit.tsx │ │ └── partials │ │ │ ├── delete-user-form.tsx │ │ │ ├── update-password-form.tsx │ │ │ └── update-profile-form.tsx │ └── welcome.tsx ├── ssr.tsx └── types │ ├── formRequests.ts │ ├── global.d.ts │ ├── index.d.ts │ ├── model.ts │ ├── param.ts │ ├── route.d.ts │ └── vite-env.d.ts ├── package.json ├── phpunit.xml ├── postcss.config.mjs ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── resources └── views │ └── app.blade.php ├── routes ├── auth.php ├── console.php └── web.php ├── setup.sh ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore ├── logs │ └── .gitignore └── pail │ └── .gitignore ├── tailwind.config.ts ├── tests ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ ├── PasswordUpdateTest.php │ │ └── RegistrationTest.php │ ├── ExampleTest.php │ └── ProfileTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | # APP_MAINTENANCE_STORE=database 14 | 15 | PHP_CLI_SERVER_WORKERS=4 16 | 17 | BCRYPT_ROUNDS=12 18 | 19 | LOG_CHANNEL=stack 20 | LOG_STACK=single 21 | LOG_DEPRECATIONS_CHANNEL=null 22 | LOG_LEVEL=debug 23 | 24 | DB_CONNECTION=sqlite 25 | # DB_HOST=127.0.0.1 26 | # DB_PORT=3306 27 | # DB_DATABASE=laravel 28 | # DB_USERNAME=root 29 | # DB_PASSWORD= 30 | 31 | SESSION_DRIVER=database 32 | SESSION_LIFETIME=120 33 | SESSION_ENCRYPT=false 34 | SESSION_PATH=/ 35 | SESSION_DOMAIN=null 36 | 37 | BROADCAST_CONNECTION=log 38 | FILESYSTEM_DISK=local 39 | QUEUE_CONNECTION=database 40 | 41 | CACHE_STORE=database 42 | CACHE_PREFIX= 43 | 44 | MEMCACHED_HOST=127.0.0.1 45 | 46 | REDIS_CLIENT=phpredis 47 | REDIS_HOST=127.0.0.1 48 | REDIS_PASSWORD=null 49 | REDIS_PORT=6379 50 | 51 | MAIL_MAILER=log 52 | MAIL_HOST=127.0.0.1 53 | MAIL_PORT=2525 54 | MAIL_USERNAME=null 55 | MAIL_PASSWORD=null 56 | MAIL_ENCRYPTION=null 57 | MAIL_FROM_ADDRESS="hello@example.com" 58 | MAIL_FROM_NAME="${APP_NAME}" 59 | 60 | AWS_ACCESS_KEY_ID= 61 | AWS_SECRET_ACCESS_KEY= 62 | AWS_DEFAULT_REGION=us-east-1 63 | AWS_BUCKET= 64 | AWS_USE_PATH_STYLE_ENDPOINT=false 65 | 66 | VITE_APP_NAME="${APP_NAME}" 67 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /bootstrap/ssr 3 | /node_modules 4 | /public/build 5 | /public/hot 6 | /public/storage 7 | /storage/*.key 8 | /storage/pail 9 | /vendor 10 | .env 11 | .env.backup 12 | .env.production 13 | .phpactor.json 14 | .phpunit.result.cache 15 | Homestead.json 16 | Homestead.yaml 17 | auth.json 18 | npm-debug.log 19 | yarn-error.log 20 | /.fleet 21 | /.idea 22 | /.vscode 23 | /.zed 24 | _ide_helper.php 25 | _ide_helper_models.php 26 | .bun.lockb 27 | package-lock.json 28 | composer.lock 29 | bun.lockb 30 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel + React + TypeScript + shadcn/ui Starter Template 2 | 3 | [![Laravel](https://img.shields.io/badge/Laravel-11.x-FF2D20?logo=laravel)](https://laravel.com) 4 | [![React](https://img.shields.io/badge/React-18.x-61DAFB?logo=react)](https://reactjs.org) 5 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript)](https://www.typescriptlang.org) 6 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) 7 | 8 | A modern, production-ready full-stack web application starter template combining Laravel's robust backend with React's powerful frontend capabilities, enhanced with TypeScript and beautiful shadcn/ui components. 9 | 10 | ## 🌟 Features 11 | 12 | - **Backend Excellence** 13 | 14 | - 🎯 Laravel 11.x with modern PHP 8.3+ features 15 | - 🔒 Built-in authentication system via Laravel Breeze 16 | - 🚀 RESTful API support with structured responses 17 | - 📝 Comprehensive database migrations and seeders 18 | 19 | - **Frontend Power** 20 | - ⚛️ React 18 with TypeScript for type-safe development 21 | - 🎨 Pre-configured shadcn/ui components 22 | - 🔄 Seamless SPA experience with Inertia.js 23 | - 🎭 Lightning-fast HMR with Vite 24 | - 🎯 Responsive layouts with Tailwind CSS 25 | 26 | ## 🚀 Prerequisites 27 | 28 | - PHP 8.2 or higher 29 | - Node.js 18 or higher 30 | - Composer 2.x 31 | - MySQL 8.0+ or PostgreSQL 13+ 32 | 33 | ## ⚙️ Installation 34 | 35 | 1. Clone the repository: 36 | 37 | ```sh 38 | git clone https://github.com/obenchkroune/laravel-shadcn-starter 39 | cd laravel-shadcn-starter 40 | ``` 41 | 42 | 2. Run the setup script: 43 | 44 | ```sh 45 | chmod +x setup.sh 46 | ./setup.sh 47 | ``` 48 | 49 | ## 🔧 Development 50 | 51 | Start the development server: 52 | 53 | ```sh 54 | composer run-script dev 55 | ``` 56 | 57 | ## 📜 Available Scripts 58 | 59 | - `npm run build` - Build for production 60 | - `composer run-script dev` - Start Laravel + Vite development server in addition to the typescript types generator 61 | 62 | ## Typescript types generation 63 | 64 | - generate typescript types for 65 | - Laravel Models 66 | - PHP enums `app/Enums/**/*.php` 67 | - Routes name and parameters types `route()` 68 | - Form Requests `app/Http/Requests` 69 | 70 | ## 🤝 Contributing 71 | 72 | Contributions are welcome! Please open an issue or submit a pull request for any changes. 73 | 74 | ## 🐛 Troubleshooting 75 | 76 | If you encounter any issues, please check the following: 77 | 78 | - Ensure all prerequisites are installed and versions are correct. 79 | - Check the `.env` file for correct configuration. 80 | - Review the Laravel and React documentation for common issues. 81 | 82 | ## 🛠️ Tech Stack 83 | 84 | - [Laravel](https://laravel.com/) 85 | - [React](https://reactjs.org/) 86 | - [TypeScript](https://www.typescriptlang.org/) 87 | - [shadcn/ui](https://ui.shadcn.com/) 88 | - [Tailwind CSS](https://tailwindcss.com/) 89 | - [Inertia.js](https://inertiajs.com/) 90 | - [Vite](https://vitejs.dev/) 91 | 92 | ## 📄 License 93 | 94 | MIT 95 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | Route::has('password.request'), 23 | 'status' => session('status'), 24 | ]); 25 | } 26 | 27 | /** 28 | * Handle an incoming authentication request. 29 | */ 30 | public function store(LoginRequest $request): RedirectResponse 31 | { 32 | $request->authenticate(); 33 | 34 | $request->session()->regenerate(); 35 | 36 | return redirect()->intended(route('dashboard', absolute: false)); 37 | } 38 | 39 | /** 40 | * Destroy an authenticated session. 41 | */ 42 | public function destroy(Request $request): RedirectResponse 43 | { 44 | Auth::guard('web')->logout(); 45 | 46 | $request->session()->invalidate(); 47 | 48 | $request->session()->regenerateToken(); 49 | 50 | return redirect('/'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 29 | 'email' => $request->user()->email, 30 | 'password' => $request->password, 31 | ])) { 32 | throw ValidationException::withMessages([ 33 | 'password' => __('auth.password'), 34 | ]); 35 | } 36 | 37 | $request->session()->put('auth.password_confirmed_at', time()); 38 | 39 | return redirect()->intended(route('dashboard', absolute: false)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 17 | return redirect()->intended(route('dashboard', absolute: false)); 18 | } 19 | 20 | $request->user()->sendEmailVerificationNotification(); 21 | 22 | return back()->with('status', 'verification-link-sent'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 19 | ? redirect()->intended(route('dashboard', absolute: false)) 20 | : Inertia::render('auth/verify-email', ['status' => session('status')]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request->email, 26 | 'token' => $request->route('token'), 27 | ]); 28 | } 29 | 30 | /** 31 | * Handle an incoming new password request. 32 | * 33 | * @throws \Illuminate\Validation\ValidationException 34 | */ 35 | public function store(Request $request): RedirectResponse 36 | { 37 | $request->validate([ 38 | 'token' => 'required', 39 | 'email' => 'required|email', 40 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 41 | ]); 42 | 43 | // Here we will attempt to reset the user's password. If it is successful we 44 | // will update the password on an actual user model and persist it to the 45 | // database. Otherwise we will parse the error and return the response. 46 | $status = Password::reset( 47 | $request->only('email', 'password', 'password_confirmation', 'token'), 48 | function ($user) use ($request) { 49 | $user->forceFill([ 50 | 'password' => Hash::make($request->password), 51 | 'remember_token' => Str::random(60), 52 | ])->save(); 53 | 54 | event(new PasswordReset($user)); 55 | } 56 | ); 57 | 58 | // If the password was successfully reset, we will redirect the user back to 59 | // the application's home authenticated view. If there is an error we can 60 | // redirect them back to where they came from with their error message. 61 | if ($status == Password::PASSWORD_RESET) { 62 | return redirect()->route('login')->with('status', __($status)); 63 | } 64 | 65 | throw ValidationException::withMessages([ 66 | 'email' => [trans($status)], 67 | ]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 19 | 'current_password' => ['required', 'current_password'], 20 | 'password' => ['required', Password::defaults(), 'confirmed'], 21 | ]); 22 | 23 | $request->user()->update([ 24 | 'password' => Hash::make($validated['password']), 25 | ]); 26 | 27 | return back(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | session('status'), 22 | ]); 23 | } 24 | 25 | /** 26 | * Handle an incoming password reset link request. 27 | * 28 | * @throws \Illuminate\Validation\ValidationException 29 | */ 30 | public function store(Request $request): RedirectResponse 31 | { 32 | $request->validate([ 33 | 'email' => 'required|email', 34 | ]); 35 | 36 | // We will send the password reset link to this user. Once we have attempted 37 | // to send the link, we will examine the response then see the message we 38 | // need to show to the user. Finally, we'll send out a proper response. 39 | $status = Password::sendResetLink( 40 | $request->only('email') 41 | ); 42 | 43 | if ($status == Password::RESET_LINK_SENT) { 44 | return back()->with('status', __($status)); 45 | } 46 | 47 | throw ValidationException::withMessages([ 48 | 'email' => [trans($status)], 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 34 | 'name' => 'required|string|max:255', 35 | 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, 36 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 37 | ]); 38 | 39 | $user = User::create([ 40 | 'name' => $request->name, 41 | 'email' => $request->email, 42 | 'password' => Hash::make($request->password), 43 | ]); 44 | 45 | event(new Registered($user)); 46 | 47 | Auth::login($user); 48 | 49 | return redirect(route('dashboard', absolute: false)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 18 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 19 | } 20 | 21 | if ($request->user()->markEmailAsVerified()) { 22 | event(new Verified($request->user())); 23 | } 24 | 25 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | $request->user() instanceof MustVerifyEmail, 23 | 'status' => session('status'), 24 | ]); 25 | } 26 | 27 | /** 28 | * Update the user's profile information. 29 | */ 30 | public function update(ProfileUpdateRequest $request): RedirectResponse 31 | { 32 | $request->user()->fill($request->validated()); 33 | 34 | if ($request->user()->isDirty('email')) { 35 | $request->user()->email_verified_at = null; 36 | } 37 | 38 | $request->user()->save(); 39 | 40 | return Redirect::route('profile.edit'); 41 | } 42 | 43 | /** 44 | * Delete the user's account. 45 | */ 46 | public function destroy(Request $request): RedirectResponse 47 | { 48 | $request->validate([ 49 | 'password' => ['required', 'current_password'], 50 | ]); 51 | 52 | $user = $request->user(); 53 | 54 | Auth::logout(); 55 | 56 | $user->delete(); 57 | 58 | $request->session()->invalidate(); 59 | $request->session()->regenerateToken(); 60 | 61 | return Redirect::to('/'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function share(Request $request): array 32 | { 33 | return [ 34 | ...parent::share($request), 35 | 'auth' => [ 36 | 'user' => $request->user(), 37 | ], 38 | 'ziggy' => fn () => [ 39 | ...(new Ziggy)->toArray(), 40 | 'location' => $request->url(), 41 | ], 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function rules(): array 28 | { 29 | return [ 30 | 'email' => ['required', 'string', 'email'], 31 | 'password' => ['required', 'string'], 32 | ]; 33 | } 34 | 35 | /** 36 | * Attempt to authenticate the request's credentials. 37 | * 38 | * @throws \Illuminate\Validation\ValidationException 39 | */ 40 | public function authenticate(): void 41 | { 42 | $this->ensureIsNotRateLimited(); 43 | 44 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 45 | RateLimiter::hit($this->throttleKey()); 46 | 47 | throw ValidationException::withMessages([ 48 | 'email' => trans('auth.failed'), 49 | ]); 50 | } 51 | 52 | RateLimiter::clear($this->throttleKey()); 53 | } 54 | 55 | /** 56 | * Ensure the login request is not rate limited. 57 | * 58 | * @throws \Illuminate\Validation\ValidationException 59 | */ 60 | public function ensureIsNotRateLimited(): void 61 | { 62 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 63 | return; 64 | } 65 | 66 | event(new Lockout($this)); 67 | 68 | $seconds = RateLimiter::availableIn($this->throttleKey()); 69 | 70 | throw ValidationException::withMessages([ 71 | 'email' => trans('auth.throttle', [ 72 | 'seconds' => $seconds, 73 | 'minutes' => ceil($seconds / 60), 74 | ]), 75 | ]); 76 | } 77 | 78 | /** 79 | * Get the rate limiting throttle key for the request. 80 | */ 81 | public function throttleKey(): string 82 | { 83 | return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'name' => ['required', 'string', 'max:255'], 20 | 'email' => [ 21 | 'required', 22 | 'string', 23 | 'lowercase', 24 | 'email', 25 | 'max:255', 26 | Rule::unique(User::class)->ignore($this->user()->id), 27 | ], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 13 | use HasFactory, Notifiable; 14 | 15 | /** 16 | * The attributes that are mass assignable. 17 | * 18 | * @var array 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | ]; 25 | 26 | /** 27 | * The attributes that should be hidden for serialization. 28 | * 29 | * @var array 30 | */ 31 | protected $hidden = [ 32 | 'password', 33 | 'remember_token', 34 | ]; 35 | 36 | /** 37 | * Get the attributes that should be cast. 38 | * 39 | * @return array 40 | */ 41 | protected function casts(): array 42 | { 43 | return [ 44 | 'email_verified_at' => 'datetime', 45 | 'password' => 'hashed', 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 9 | web: __DIR__.'/../routes/web.php', 10 | commands: __DIR__.'/../routes/console.php', 11 | health: '/up', 12 | ) 13 | ->withMiddleware(function (Middleware $middleware) { 14 | $middleware->web(append: [ 15 | \App\Http\Middleware\HandleInertiaRequests::class, 16 | \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, 17 | ]); 18 | 19 | // 20 | }) 21 | ->withExceptions(function (Exceptions $exceptions) { 22 | // 23 | })->create(); 24 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Environment 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "environment" your application is currently 24 | | running in. This may determine how you prefer to configure various 25 | | services the application utilizes. Set this in your ".env" file. 26 | | 27 | */ 28 | 29 | 'env' => env('APP_ENV', 'production'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Debug Mode 34 | |-------------------------------------------------------------------------- 35 | | 36 | | When your application is in debug mode, detailed error messages with 37 | | stack traces will be shown on every error that occurs within your 38 | | application. If disabled, a simple generic error page is shown. 39 | | 40 | */ 41 | 42 | 'debug' => (bool) env('APP_DEBUG', false), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application URL 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This URL is used by the console to properly generate URLs when using 50 | | the Artisan command line tool. You should set this to the root of 51 | | the application so that it's available within Artisan commands. 52 | | 53 | */ 54 | 55 | 'url' => env('APP_URL', 'http://localhost'), 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Application Timezone 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Here you may specify the default timezone for your application, which 63 | | will be used by the PHP date and date-time functions. The timezone 64 | | is set to "UTC" by default as it is suitable for most use cases. 65 | | 66 | */ 67 | 68 | 'timezone' => env('APP_TIMEZONE', 'UTC'), 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Application Locale Configuration 73 | |-------------------------------------------------------------------------- 74 | | 75 | | The application locale determines the default locale that will be used 76 | | by Laravel's translation / localization methods. This option can be 77 | | set to any locale for which you plan to have translation strings. 78 | | 79 | */ 80 | 81 | 'locale' => env('APP_LOCALE', 'en'), 82 | 83 | 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), 84 | 85 | 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Encryption Key 90 | |-------------------------------------------------------------------------- 91 | | 92 | | This key is utilized by Laravel's encryption services and should be set 93 | | to a random, 32 character string to ensure that all encrypted values 94 | | are secure. You should do this prior to deploying the application. 95 | | 96 | */ 97 | 98 | 'cipher' => 'AES-256-CBC', 99 | 100 | 'key' => env('APP_KEY'), 101 | 102 | 'previous_keys' => [ 103 | ...array_filter( 104 | explode(',', env('APP_PREVIOUS_KEYS', '')) 105 | ), 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Maintenance Mode Driver 111 | |-------------------------------------------------------------------------- 112 | | 113 | | These configuration options determine the driver used to determine and 114 | | manage Laravel's "maintenance mode" status. The "cache" driver will 115 | | allow maintenance mode to be controlled across multiple machines. 116 | | 117 | | Supported drivers: "file", "cache" 118 | | 119 | */ 120 | 121 | 'maintenance' => [ 122 | 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 123 | 'store' => env('APP_MAINTENANCE_STORE', 'database'), 124 | ], 125 | 126 | ]; 127 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => env('AUTH_GUARD', 'web'), 18 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | which utilizes session storage plus the Eloquent user provider. 29 | | 30 | | All authentication guards have a user provider, which defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | system used by the application. Typically, Eloquent is utilized. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication guards have a user provider, which defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | system used by the application. Typically, Eloquent is utilized. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | providers to represent the model / table. These providers may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => env('AUTH_MODEL', App\Models\User::class), 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | These configuration options specify the behavior of Laravel's password 80 | | reset functionality, including the table utilized for token storage 81 | | and the user provider that is invoked to actually retrieve users. 82 | | 83 | | The expiry time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | | The throttle setting is the number of seconds a user must wait before 88 | | generating more password reset tokens. This prevents the user from 89 | | quickly generating a very large amount of password reset tokens. 90 | | 91 | */ 92 | 93 | 'passwords' => [ 94 | 'users' => [ 95 | 'provider' => 'users', 96 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 97 | 'expire' => 60, 98 | 'throttle' => 60, 99 | ], 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Password Confirmation Timeout 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Here you may define the amount of seconds before a password confirmation 108 | | window expires and users are asked to re-enter their password via the 109 | | confirmation screen. By default, the timeout lasts for three hours. 110 | | 111 | */ 112 | 113 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 114 | 115 | ]; 116 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "array", "database", "file", "memcached", 30 | | "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'array' => [ 37 | 'driver' => 'array', 38 | 'serialize' => false, 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'connection' => env('DB_CACHE_CONNECTION'), 44 | 'table' => env('DB_CACHE_TABLE', 'cache'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 47 | ], 48 | 49 | 'file' => [ 50 | 'driver' => 'file', 51 | 'path' => storage_path('framework/cache/data'), 52 | 'lock_path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | 'octane' => [ 90 | 'driver' => 'octane', 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Cache Key Prefix 98 | |-------------------------------------------------------------------------- 99 | | 100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 101 | | stores, there might be other applications using the same cache. For 102 | | that reason, you may prefix every cache key to avoid collisions. 103 | | 104 | */ 105 | 106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 107 | 108 | ]; 109 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'sqlite'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Database Connections 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Below are all of the database connections defined for your application. 27 | | An example configuration is provided for each database system which 28 | | is supported by Laravel. You're free to add / remove connections. 29 | | 30 | */ 31 | 32 | 'connections' => [ 33 | 34 | 'sqlite' => [ 35 | 'driver' => 'sqlite', 36 | 'url' => env('DB_URL'), 37 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 38 | 'prefix' => '', 39 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 40 | 'busy_timeout' => null, 41 | 'journal_mode' => null, 42 | 'synchronous' => null, 43 | ], 44 | 45 | 'mysql' => [ 46 | 'driver' => 'mysql', 47 | 'url' => env('DB_URL'), 48 | 'host' => env('DB_HOST', '127.0.0.1'), 49 | 'port' => env('DB_PORT', '3306'), 50 | 'database' => env('DB_DATABASE', 'laravel'), 51 | 'username' => env('DB_USERNAME', 'root'), 52 | 'password' => env('DB_PASSWORD', ''), 53 | 'unix_socket' => env('DB_SOCKET', ''), 54 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 55 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 56 | 'prefix' => '', 57 | 'prefix_indexes' => true, 58 | 'strict' => true, 59 | 'engine' => null, 60 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 61 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 62 | ]) : [], 63 | ], 64 | 65 | 'mariadb' => [ 66 | 'driver' => 'mariadb', 67 | 'url' => env('DB_URL'), 68 | 'host' => env('DB_HOST', '127.0.0.1'), 69 | 'port' => env('DB_PORT', '3306'), 70 | 'database' => env('DB_DATABASE', 'laravel'), 71 | 'username' => env('DB_USERNAME', 'root'), 72 | 'password' => env('DB_PASSWORD', ''), 73 | 'unix_socket' => env('DB_SOCKET', ''), 74 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 75 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 76 | 'prefix' => '', 77 | 'prefix_indexes' => true, 78 | 'strict' => true, 79 | 'engine' => null, 80 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 81 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 82 | ]) : [], 83 | ], 84 | 85 | 'pgsql' => [ 86 | 'driver' => 'pgsql', 87 | 'url' => env('DB_URL'), 88 | 'host' => env('DB_HOST', '127.0.0.1'), 89 | 'port' => env('DB_PORT', '5432'), 90 | 'database' => env('DB_DATABASE', 'laravel'), 91 | 'username' => env('DB_USERNAME', 'root'), 92 | 'password' => env('DB_PASSWORD', ''), 93 | 'charset' => env('DB_CHARSET', 'utf8'), 94 | 'prefix' => '', 95 | 'prefix_indexes' => true, 96 | 'search_path' => 'public', 97 | 'sslmode' => 'prefer', 98 | ], 99 | 100 | 'sqlsrv' => [ 101 | 'driver' => 'sqlsrv', 102 | 'url' => env('DB_URL'), 103 | 'host' => env('DB_HOST', 'localhost'), 104 | 'port' => env('DB_PORT', '1433'), 105 | 'database' => env('DB_DATABASE', 'laravel'), 106 | 'username' => env('DB_USERNAME', 'root'), 107 | 'password' => env('DB_PASSWORD', ''), 108 | 'charset' => env('DB_CHARSET', 'utf8'), 109 | 'prefix' => '', 110 | 'prefix_indexes' => true, 111 | // 'encrypt' => env('DB_ENCRYPT', 'yes'), 112 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 113 | ], 114 | 115 | ], 116 | 117 | /* 118 | |-------------------------------------------------------------------------- 119 | | Migration Repository Table 120 | |-------------------------------------------------------------------------- 121 | | 122 | | This table keeps track of all the migrations that have already run for 123 | | your application. Using this information, we can determine which of 124 | | the migrations on disk haven't actually been run on the database. 125 | | 126 | */ 127 | 128 | 'migrations' => [ 129 | 'table' => 'migrations', 130 | 'update_date_on_publish' => true, 131 | ], 132 | 133 | /* 134 | |-------------------------------------------------------------------------- 135 | | Redis Databases 136 | |-------------------------------------------------------------------------- 137 | | 138 | | Redis is an open source, fast, and advanced key-value store that also 139 | | provides a richer body of commands than a typical key-value system 140 | | such as Memcached. You may define your connection settings here. 141 | | 142 | */ 143 | 144 | 'redis' => [ 145 | 146 | 'client' => env('REDIS_CLIENT', 'phpredis'), 147 | 148 | 'options' => [ 149 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 150 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 151 | ], 152 | 153 | 'default' => [ 154 | 'url' => env('REDIS_URL'), 155 | 'host' => env('REDIS_HOST', '127.0.0.1'), 156 | 'username' => env('REDIS_USERNAME'), 157 | 'password' => env('REDIS_PASSWORD'), 158 | 'port' => env('REDIS_PORT', '6379'), 159 | 'database' => env('REDIS_DB', '0'), 160 | ], 161 | 162 | 'cache' => [ 163 | 'url' => env('REDIS_URL'), 164 | 'host' => env('REDIS_HOST', '127.0.0.1'), 165 | 'username' => env('REDIS_USERNAME'), 166 | 'password' => env('REDIS_PASSWORD'), 167 | 'port' => env('REDIS_PORT', '6379'), 168 | 'database' => env('REDIS_CACHE_DB', '1'), 169 | ], 170 | 171 | ], 172 | 173 | ]; 174 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | ], 39 | 40 | 'public' => [ 41 | 'driver' => 'local', 42 | 'root' => storage_path('app/public'), 43 | 'url' => env('APP_URL').'/storage', 44 | 'visibility' => 'public', 45 | 'throw' => false, 46 | ], 47 | 48 | 's3' => [ 49 | 'driver' => 's3', 50 | 'key' => env('AWS_ACCESS_KEY_ID'), 51 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 52 | 'region' => env('AWS_DEFAULT_REGION'), 53 | 'bucket' => env('AWS_BUCKET'), 54 | 'url' => env('AWS_URL'), 55 | 'endpoint' => env('AWS_ENDPOINT'), 56 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 57 | 'throw' => false, 58 | ], 59 | 60 | ], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Symbolic Links 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Here you may configure the symbolic links that will be created when the 68 | | `storage:link` Artisan command is executed. The array keys should be 69 | | the locations of the links and the values should be their targets. 70 | | 71 | */ 72 | 73 | 'links' => [ 74 | public_path('storage') => storage_path('app/public'), 75 | ], 76 | 77 | ]; 78 | -------------------------------------------------------------------------------- /config/ide-helper.php: -------------------------------------------------------------------------------- 1 | '_ide_helper.php', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Models filename 19 | |-------------------------------------------------------------------------- 20 | | 21 | | The default filename for the models helper file. 22 | | 23 | */ 24 | 25 | 'models_filename' => '_ide_helper_models.php', 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | PhpStorm meta filename 30 | |-------------------------------------------------------------------------- 31 | | 32 | | PhpStorm also supports the directory `.phpstorm.meta.php/` with arbitrary 33 | | files in it, should you need additional files for your project; e.g. 34 | | `.phpstorm.meta.php/laravel_ide_Helper.php'. 35 | | 36 | */ 37 | 'meta_filename' => '.phpstorm.meta.php', 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Fluent helpers 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Set to true to generate commonly used Fluent methods. 45 | | 46 | */ 47 | 48 | 'include_fluent' => true, 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Factory builders 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Set to true to generate factory generators for better factory() 56 | | method auto-completion. 57 | | 58 | | Deprecated for Laravel 8 or latest. 59 | | 60 | */ 61 | 62 | 'include_factory_builders' => true, 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Write model magic methods 67 | |-------------------------------------------------------------------------- 68 | | 69 | | Set to false to disable write magic methods of model. 70 | | 71 | */ 72 | 73 | 'write_model_magic_where' => true, 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Write model external Eloquent builder methods 78 | |-------------------------------------------------------------------------- 79 | | 80 | | Set to false to disable write external Eloquent builder methods. 81 | | 82 | */ 83 | 84 | 'write_model_external_builder_methods' => true, 85 | 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | Write model relation count properties 89 | |-------------------------------------------------------------------------- 90 | | 91 | | Set to false to disable writing of relation count properties to model DocBlocks. 92 | | 93 | */ 94 | 95 | 'write_model_relation_count_properties' => true, 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Write Eloquent model mixins 100 | |-------------------------------------------------------------------------- 101 | | 102 | | This will add the necessary DocBlock mixins to the model class 103 | | contained in the Laravel framework. This helps the IDE with 104 | | auto-completion. 105 | | 106 | | Please be aware that this setting changes a file within the /vendor directory. 107 | | 108 | */ 109 | 110 | 'write_eloquent_model_mixins' => true, 111 | 112 | /* 113 | |-------------------------------------------------------------------------- 114 | | Helper files to include 115 | |-------------------------------------------------------------------------- 116 | | 117 | | Include helper files. By default not included, but can be toggled with the 118 | | -- helpers (-H) option. Extra helper files can be included. 119 | | 120 | */ 121 | 122 | 'include_helpers' => true, 123 | 124 | 'helper_files' => [ 125 | base_path().'/vendor/laravel/framework/src/Illuminate/Support/helpers.php', 126 | ], 127 | 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | Model locations to include 131 | |-------------------------------------------------------------------------- 132 | | 133 | | Define in which directories the ide-helper:models command should look 134 | | for models. 135 | | 136 | | glob patterns are supported to easier reach models in sub-directories, 137 | | e.g. `app/Services/* /Models` (without the space). 138 | | 139 | */ 140 | 141 | 'model_locations' => [ 142 | 'app/Models', 143 | ], 144 | 145 | /* 146 | |-------------------------------------------------------------------------- 147 | | Models to ignore 148 | |-------------------------------------------------------------------------- 149 | | 150 | | Define which models should be ignored. 151 | | 152 | */ 153 | 154 | 'ignored_models' => [ 155 | // App\MyModel::class, 156 | ], 157 | 158 | /* 159 | |-------------------------------------------------------------------------- 160 | | Models hooks 161 | |-------------------------------------------------------------------------- 162 | | 163 | | Define which hook classes you want to run for models to add custom information. 164 | | 165 | | Hooks should implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface. 166 | | 167 | */ 168 | 169 | 'model_hooks' => [ 170 | // App\Support\IdeHelper\MyModelHook::class 171 | ], 172 | 173 | /* 174 | |-------------------------------------------------------------------------- 175 | | Extra classes 176 | |-------------------------------------------------------------------------- 177 | | 178 | | These implementations are not really extended, but called with magic functions. 179 | | 180 | */ 181 | 182 | 'extra' => [ 183 | 'Eloquent' => ['Illuminate\Database\Eloquent\Builder', 'Illuminate\Database\Query\Builder'], 184 | 'Session' => ['Illuminate\Session\Store'], 185 | ], 186 | 187 | 'magic' => [], 188 | 189 | /* 190 | |-------------------------------------------------------------------------- 191 | | Interface implementations 192 | |-------------------------------------------------------------------------- 193 | | 194 | | These interfaces will be replaced with the implementing class. Some interfaces 195 | | are detected by the helpers, others can be listed below. 196 | | 197 | */ 198 | 199 | 'interfaces' => [ 200 | // App\MyInterface::class => App\MyImplementation::class, 201 | ], 202 | 203 | /* 204 | |-------------------------------------------------------------------------- 205 | | Support for camel cased models 206 | |-------------------------------------------------------------------------- 207 | | 208 | | There are some Laravel packages (such as Eloquence) that allow for accessing 209 | | Eloquent model properties via camel case, instead of snake case. 210 | | 211 | | Enabling this option will support these packages by saving all model 212 | | properties as camel case, instead of snake case. 213 | | 214 | | For example, normally you would see this: 215 | | 216 | | * @property \Illuminate\Support\Carbon $created_at 217 | | * @property \Illuminate\Support\Carbon $updated_at 218 | | 219 | | With this enabled, the properties will be this: 220 | | 221 | | * @property \Illuminate\Support\Carbon $createdAt 222 | | * @property \Illuminate\Support\Carbon $updatedAt 223 | | 224 | | Note, it is currently an all-or-nothing option. 225 | | 226 | */ 227 | 'model_camel_case_properties' => false, 228 | 229 | /* 230 | |-------------------------------------------------------------------------- 231 | | Property casts 232 | |-------------------------------------------------------------------------- 233 | | 234 | | Cast the given "real type" to the given "type". 235 | | 236 | */ 237 | 'type_overrides' => [ 238 | 'integer' => 'int', 239 | 'boolean' => 'bool', 240 | ], 241 | 242 | /* 243 | |-------------------------------------------------------------------------- 244 | | Include DocBlocks from classes 245 | |-------------------------------------------------------------------------- 246 | | 247 | | Include DocBlocks from classes to allow additional code inspection for 248 | | magic methods and properties. 249 | | 250 | */ 251 | 'include_class_docblocks' => false, 252 | 253 | /* 254 | |-------------------------------------------------------------------------- 255 | | Force FQN usage 256 | |-------------------------------------------------------------------------- 257 | | 258 | | Use the fully qualified (class) name in DocBlocks, 259 | | even if the class exists in the same namespace 260 | | or there is an import (use className) of the class. 261 | | 262 | */ 263 | 'force_fqn' => false, 264 | 265 | /* 266 | |-------------------------------------------------------------------------- 267 | | Use generics syntax 268 | |-------------------------------------------------------------------------- 269 | | 270 | | Use generics syntax within DocBlocks, 271 | | e.g. `Collection` instead of `Collection|User[]`. 272 | | 273 | */ 274 | 'use_generics_annotations' => true, 275 | 276 | /* 277 | |-------------------------------------------------------------------------- 278 | | Additional relation types 279 | |-------------------------------------------------------------------------- 280 | | 281 | | Sometimes it's needed to create custom relation types. The key of the array 282 | | is the relationship method name. The value of the array is the fully-qualified 283 | | class name of the relationship, e.g. `'relationName' => RelationShipClass::class`. 284 | | 285 | */ 286 | 'additional_relation_types' => [], 287 | 288 | /* 289 | |-------------------------------------------------------------------------- 290 | | Additional relation return types 291 | |-------------------------------------------------------------------------- 292 | | 293 | | When using custom relation types its possible for the class name to not contain 294 | | the proper return type of the relation. The key of the array is the relationship 295 | | method name. The value of the array is the return type of the relation ('many' 296 | | or 'morphTo'). 297 | | e.g. `'relationName' => 'many'`. 298 | | 299 | */ 300 | 'additional_relation_return_types' => [], 301 | 302 | /* 303 | |-------------------------------------------------------------------------- 304 | | Run artisan commands after migrations to generate model helpers 305 | |-------------------------------------------------------------------------- 306 | | 307 | | The specified commands should run after migrations are finished running. 308 | | 309 | */ 310 | 'post_migrate' => [ 311 | 'ide-helper:models --nowrite', 312 | ], 313 | 314 | ]; 315 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Deprecations Log Channel 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This option controls the log channel that should be used to log warnings 29 | | regarding deprecated PHP and library features. This allows you to get 30 | | your application ready for upcoming major versions of dependencies. 31 | | 32 | */ 33 | 34 | 'deprecations' => [ 35 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 36 | 'trace' => env('LOG_DEPRECATIONS_TRACE', false), 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Log Channels 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Here you may configure the log channels for your application. Laravel 45 | | utilizes the Monolog PHP logging library, which includes a variety 46 | | of powerful log handlers and formatters that you're free to use. 47 | | 48 | | Available drivers: "single", "daily", "slack", "syslog", 49 | | "errorlog", "monolog", "custom", "stack" 50 | | 51 | */ 52 | 53 | 'channels' => [ 54 | 55 | 'stack' => [ 56 | 'driver' => 'stack', 57 | 'channels' => explode(',', env('LOG_STACK', 'single')), 58 | 'ignore_exceptions' => false, 59 | ], 60 | 61 | 'single' => [ 62 | 'driver' => 'single', 63 | 'path' => storage_path('logs/laravel.log'), 64 | 'level' => env('LOG_LEVEL', 'debug'), 65 | 'replace_placeholders' => true, 66 | ], 67 | 68 | 'daily' => [ 69 | 'driver' => 'daily', 70 | 'path' => storage_path('logs/laravel.log'), 71 | 'level' => env('LOG_LEVEL', 'debug'), 72 | 'days' => env('LOG_DAILY_DAYS', 14), 73 | 'replace_placeholders' => true, 74 | ], 75 | 76 | 'slack' => [ 77 | 'driver' => 'slack', 78 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 79 | 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), 80 | 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 81 | 'level' => env('LOG_LEVEL', 'critical'), 82 | 'replace_placeholders' => true, 83 | ], 84 | 85 | 'papertrail' => [ 86 | 'driver' => 'monolog', 87 | 'level' => env('LOG_LEVEL', 'debug'), 88 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 89 | 'handler_with' => [ 90 | 'host' => env('PAPERTRAIL_URL'), 91 | 'port' => env('PAPERTRAIL_PORT'), 92 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 93 | ], 94 | 'processors' => [PsrLogMessageProcessor::class], 95 | ], 96 | 97 | 'stderr' => [ 98 | 'driver' => 'monolog', 99 | 'level' => env('LOG_LEVEL', 'debug'), 100 | 'handler' => StreamHandler::class, 101 | 'formatter' => env('LOG_STDERR_FORMATTER'), 102 | 'with' => [ 103 | 'stream' => 'php://stderr', 104 | ], 105 | 'processors' => [PsrLogMessageProcessor::class], 106 | ], 107 | 108 | 'syslog' => [ 109 | 'driver' => 'syslog', 110 | 'level' => env('LOG_LEVEL', 'debug'), 111 | 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 112 | 'replace_placeholders' => true, 113 | ], 114 | 115 | 'errorlog' => [ 116 | 'driver' => 'errorlog', 117 | 'level' => env('LOG_LEVEL', 'debug'), 118 | 'replace_placeholders' => true, 119 | ], 120 | 121 | 'null' => [ 122 | 'driver' => 'monolog', 123 | 'handler' => NullHandler::class, 124 | ], 125 | 126 | 'emergency' => [ 127 | 'path' => storage_path('logs/laravel.log'), 128 | ], 129 | 130 | ], 131 | 132 | ]; 133 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Mailer Configurations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may configure all of the mailers used by your application plus 25 | | their respective settings. Several examples have been configured for 26 | | you and you are free to add your own as your application requires. 27 | | 28 | | Laravel supports a variety of mail "transport" drivers that can be used 29 | | when delivering an email. You may specify which one you're using for 30 | | your mailers below. You may also add additional mailers if needed. 31 | | 32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 33 | | "postmark", "resend", "log", "array", 34 | | "failover", "roundrobin" 35 | | 36 | */ 37 | 38 | 'mailers' => [ 39 | 40 | 'smtp' => [ 41 | 'transport' => 'smtp', 42 | 'url' => env('MAIL_URL'), 43 | 'host' => env('MAIL_HOST', '127.0.0.1'), 44 | 'port' => env('MAIL_PORT', 2525), 45 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 46 | 'username' => env('MAIL_USERNAME'), 47 | 'password' => env('MAIL_PASSWORD'), 48 | 'timeout' => null, 49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 50 | ], 51 | 52 | 'ses' => [ 53 | 'transport' => 'ses', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 59 | // 'client' => [ 60 | // 'timeout' => 5, 61 | // ], 62 | ], 63 | 64 | 'resend' => [ 65 | 'transport' => 'resend', 66 | ], 67 | 68 | 'sendmail' => [ 69 | 'transport' => 'sendmail', 70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 71 | ], 72 | 73 | 'log' => [ 74 | 'transport' => 'log', 75 | 'channel' => env('MAIL_LOG_CHANNEL'), 76 | ], 77 | 78 | 'array' => [ 79 | 'transport' => 'array', 80 | ], 81 | 82 | 'failover' => [ 83 | 'transport' => 'failover', 84 | 'mailers' => [ 85 | 'smtp', 86 | 'log', 87 | ], 88 | ], 89 | 90 | 'roundrobin' => [ 91 | 'transport' => 'roundrobin', 92 | 'mailers' => [ 93 | 'ses', 94 | 'postmark', 95 | ], 96 | ], 97 | 98 | ], 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Global "From" Address 103 | |-------------------------------------------------------------------------- 104 | | 105 | | You may wish for all emails sent by your application to be sent from 106 | | the same address. Here you may specify a name and address that is 107 | | used globally for all emails that are sent by your application. 108 | | 109 | */ 110 | 111 | 'from' => [ 112 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 113 | 'name' => env('MAIL_FROM_NAME', 'Example'), 114 | ], 115 | 116 | ]; 117 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection options for every queue backend 24 | | used by your application. An example configuration is provided for 25 | | each backend supported by Laravel. You're also free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'connection' => env('DB_QUEUE_CONNECTION'), 40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 41 | 'queue' => env('DB_QUEUE', 'default'), 42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 43 | 'after_commit' => false, 44 | ], 45 | 46 | 'beanstalkd' => [ 47 | 'driver' => 'beanstalkd', 48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 51 | 'block_for' => 0, 52 | 'after_commit' => false, 53 | ], 54 | 55 | 'sqs' => [ 56 | 'driver' => 'sqs', 57 | 'key' => env('AWS_ACCESS_KEY_ID'), 58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 60 | 'queue' => env('SQS_QUEUE', 'default'), 61 | 'suffix' => env('SQS_SUFFIX'), 62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 63 | 'after_commit' => false, 64 | ], 65 | 66 | 'redis' => [ 67 | 'driver' => 'redis', 68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 69 | 'queue' => env('REDIS_QUEUE', 'default'), 70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 71 | 'block_for' => null, 72 | 'after_commit' => false, 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Job Batching 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The following options configure the database and table that store job 83 | | batching information. These options can be updated to any database 84 | | connection and table which has been defined by your application. 85 | | 86 | */ 87 | 88 | 'batching' => [ 89 | 'database' => env('DB_CONNECTION', 'sqlite'), 90 | 'table' => 'job_batches', 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Failed Queue Jobs 96 | |-------------------------------------------------------------------------- 97 | | 98 | | These options configure the behavior of failed queue job logging so you 99 | | can control how and where failed jobs are stored. Laravel ships with 100 | | support for storing failed jobs in a simple file or in a database. 101 | | 102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 103 | | 104 | */ 105 | 106 | 'failed' => [ 107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 108 | 'database' => env('DB_CONNECTION', 'sqlite'), 109 | 'table' => 'failed_jobs', 110 | ], 111 | 112 | ]; 113 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | env('SESSION_DRIVER', 'database'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Session Lifetime 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may specify the number of minutes that you wish the session 29 | | to be allowed to remain idle before it expires. If you want them 30 | | to expire immediately when the browser is closed then you may 31 | | indicate that via the expire_on_close configuration option. 32 | | 33 | */ 34 | 35 | 'lifetime' => env('SESSION_LIFETIME', 120), 36 | 37 | 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Session Encryption 42 | |-------------------------------------------------------------------------- 43 | | 44 | | This option allows you to easily specify that all of your session data 45 | | should be encrypted before it's stored. All encryption is performed 46 | | automatically by Laravel and you may use the session like normal. 47 | | 48 | */ 49 | 50 | 'encrypt' => env('SESSION_ENCRYPT', false), 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Session File Location 55 | |-------------------------------------------------------------------------- 56 | | 57 | | When utilizing the "file" session driver, the session files are placed 58 | | on disk. The default storage location is defined here; however, you 59 | | are free to provide another location where they should be stored. 60 | | 61 | */ 62 | 63 | 'files' => storage_path('framework/sessions'), 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Session Database Connection 68 | |-------------------------------------------------------------------------- 69 | | 70 | | When using the "database" or "redis" session drivers, you may specify a 71 | | connection that should be used to manage these sessions. This should 72 | | correspond to a connection in your database configuration options. 73 | | 74 | */ 75 | 76 | 'connection' => env('SESSION_CONNECTION'), 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Session Database Table 81 | |-------------------------------------------------------------------------- 82 | | 83 | | When using the "database" session driver, you may specify the table to 84 | | be used to store sessions. Of course, a sensible default is defined 85 | | for you; however, you're welcome to change this to another table. 86 | | 87 | */ 88 | 89 | 'table' => env('SESSION_TABLE', 'sessions'), 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Session Cache Store 94 | |-------------------------------------------------------------------------- 95 | | 96 | | When using one of the framework's cache driven session backends, you may 97 | | define the cache store which should be used to store the session data 98 | | between requests. This must match one of your defined cache stores. 99 | | 100 | | Affects: "apc", "dynamodb", "memcached", "redis" 101 | | 102 | */ 103 | 104 | 'store' => env('SESSION_STORE'), 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Session Sweeping Lottery 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Some session drivers must manually sweep their storage location to get 112 | | rid of old sessions from storage. Here are the chances that it will 113 | | happen on a given request. By default, the odds are 2 out of 100. 114 | | 115 | */ 116 | 117 | 'lottery' => [2, 100], 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Session Cookie Name 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Here you may change the name of the session cookie that is created by 125 | | the framework. Typically, you should not need to change this value 126 | | since doing so does not grant a meaningful security improvement. 127 | | 128 | */ 129 | 130 | 'cookie' => env( 131 | 'SESSION_COOKIE', 132 | Str::slug(env('APP_NAME', 'laravel'), '_').'_session' 133 | ), 134 | 135 | /* 136 | |-------------------------------------------------------------------------- 137 | | Session Cookie Path 138 | |-------------------------------------------------------------------------- 139 | | 140 | | The session cookie path determines the path for which the cookie will 141 | | be regarded as available. Typically, this will be the root path of 142 | | your application, but you're free to change this when necessary. 143 | | 144 | */ 145 | 146 | 'path' => env('SESSION_PATH', '/'), 147 | 148 | /* 149 | |-------------------------------------------------------------------------- 150 | | Session Cookie Domain 151 | |-------------------------------------------------------------------------- 152 | | 153 | | This value determines the domain and subdomains the session cookie is 154 | | available to. By default, the cookie will be available to the root 155 | | domain and all subdomains. Typically, this shouldn't be changed. 156 | | 157 | */ 158 | 159 | 'domain' => env('SESSION_DOMAIN'), 160 | 161 | /* 162 | |-------------------------------------------------------------------------- 163 | | HTTPS Only Cookies 164 | |-------------------------------------------------------------------------- 165 | | 166 | | By setting this option to true, session cookies will only be sent back 167 | | to the server if the browser has a HTTPS connection. This will keep 168 | | the cookie from being sent to you when it can't be done securely. 169 | | 170 | */ 171 | 172 | 'secure' => env('SESSION_SECURE_COOKIE'), 173 | 174 | /* 175 | |-------------------------------------------------------------------------- 176 | | HTTP Access Only 177 | |-------------------------------------------------------------------------- 178 | | 179 | | Setting this value to true will prevent JavaScript from accessing the 180 | | value of the cookie and the cookie will only be accessible through 181 | | the HTTP protocol. It's unlikely you should disable this option. 182 | | 183 | */ 184 | 185 | 'http_only' => env('SESSION_HTTP_ONLY', true), 186 | 187 | /* 188 | |-------------------------------------------------------------------------- 189 | | Same-Site Cookies 190 | |-------------------------------------------------------------------------- 191 | | 192 | | This option determines how your cookies behave when cross-site requests 193 | | take place, and can be used to mitigate CSRF attacks. By default, we 194 | | will set this value to "lax" to permit secure cross-site requests. 195 | | 196 | | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value 197 | | 198 | | Supported: "lax", "strict", "none", null 199 | | 200 | */ 201 | 202 | 'same_site' => env('SESSION_SAME_SITE', 'lax'), 203 | 204 | /* 205 | |-------------------------------------------------------------------------- 206 | | Partitioned Cookies 207 | |-------------------------------------------------------------------------- 208 | | 209 | | Setting this value to true will tie the cookie to the top-level site for 210 | | a cross-site context. Partitioned cookies are accepted by the browser 211 | | when flagged "secure" and the Same-Site attribute is set to "none". 212 | | 213 | */ 214 | 215 | 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), 216 | 217 | ]; 218 | -------------------------------------------------------------------------------- /config/typescript-transformer.php: -------------------------------------------------------------------------------- 1 | [ 10 | app_path(), 11 | ], 12 | 13 | /* 14 | * Collectors will search for classes in the `auto_discover_types` paths and choose the correct 15 | * transformer to transform them. By default, we include a DefaultCollector which will search 16 | * for @typescript annotated and #[TypeScript] attributed classes to transform. 17 | */ 18 | 19 | 'collectors' => [ 20 | Spatie\TypeScriptTransformer\Collectors\DefaultCollector::class, 21 | Spatie\TypeScriptTransformer\Collectors\EnumCollector::class, 22 | ], 23 | 24 | /* 25 | * Transformers take PHP classes(e.g., enums) as an input and will output 26 | * a TypeScript representation of the PHP class. 27 | */ 28 | 29 | 'transformers' => [ 30 | Spatie\LaravelTypeScriptTransformer\Transformers\SpatieStateTransformer::class, 31 | Spatie\TypeScriptTransformer\Transformers\EnumTransformer::class, 32 | Spatie\TypeScriptTransformer\Transformers\SpatieEnumTransformer::class, 33 | Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer::class, 34 | Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptTransformer::class, 35 | ], 36 | 37 | /* 38 | * In your classes, you sometimes have types that should always be replaced 39 | * by the same TypeScript representations. For example, you can replace a 40 | * Datetime always with a string. You define these replacements here. 41 | */ 42 | 43 | 'default_type_replacements' => [ 44 | DateTime::class => 'string', 45 | DateTimeImmutable::class => 'string', 46 | Carbon\CarbonInterface::class => 'string', 47 | Carbon\CarbonImmutable::class => 'string', 48 | Carbon\Carbon::class => 'string', 49 | ], 50 | 51 | /* 52 | * The package will write the generated TypeScript to this file. 53 | */ 54 | 55 | 'output_file' => base_path('inertia/types/generated.d.ts'), 56 | 57 | /* 58 | * When the package is writing types to the output file, a writer is used to 59 | * determine the format. By default, this is the `TypeDefinitionWriter`. 60 | * But you can also use the `ModuleWriter` or implement your own. 61 | */ 62 | 63 | 'writer' => Spatie\TypeScriptTransformer\Writers\TypeDefinitionWriter::class, 64 | 65 | /* 66 | * The generated TypeScript file can be formatted. We ship a Prettier formatter 67 | * out of the box: `PrettierFormatter` but you can also implement your own one. 68 | * The generated TypeScript will not be formatted when no formatter was set. 69 | */ 70 | 71 | 'formatter' => null, 72 | 73 | /* 74 | * Enums can be transformed into types or native TypeScript enums, by default 75 | * the package will transform them to types. 76 | */ 77 | 78 | 'transform_to_native_enums' => false, 79 | 80 | /* 81 | * By default, this package will convert PHP nullable properties to TypeScript 82 | * types using a `null` type union. Setting `transform_null_to_optional` will 83 | * make them optional instead. 84 | */ 85 | 86 | 'transform_null_to_optional' => false, 87 | ]; 88 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | User::factory()->create([ 19 | 'name' => 'Test User', 20 | 'email' => 'test@example.com', 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /inertia/app.tsx: -------------------------------------------------------------------------------- 1 | import "./css/app.css"; 2 | import "./bootstrap"; 3 | 4 | import { createInertiaApp } from "@inertiajs/react"; 5 | import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers"; 6 | import { createRoot, hydrateRoot } from "react-dom/client"; 7 | import { Toaster } from "./components/ui/toaster"; 8 | 9 | const appName = import.meta.env.VITE_APP_NAME || "Laravel"; 10 | 11 | createInertiaApp({ 12 | title: (title) => `${title} - ${appName}`, 13 | resolve: (name) => 14 | resolvePageComponent( 15 | `./pages/${name}.tsx`, 16 | import.meta.glob("./pages/**/*.tsx"), 17 | ), 18 | setup({ el, App, props }) { 19 | if (import.meta.env.SSR) { 20 | hydrateRoot(el, ); 21 | return; 22 | } 23 | 24 | createRoot(el).render( 25 | <> 26 | 27 | 28 | , 29 | ); 30 | }, 31 | progress: { 32 | color: "#4B5563", 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /inertia/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | window.axios = axios; 3 | 4 | window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; 5 | -------------------------------------------------------------------------------- /inertia/components/application-logo.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from "react"; 2 | 3 | export default function ApplicationLogo(props: SVGAttributes) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /inertia/components/layouts/authenticated-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Link, usePage } from "@inertiajs/react"; 2 | import { PropsWithChildren, useState } from "react"; 3 | import { Menu, X } from "lucide-react"; 4 | import ApplicationLogo from "~/components/application-logo"; 5 | import { Button } from "~/components/ui/button"; 6 | import { Navigation } from "./navigation"; 7 | import { UserDropdown } from "./user-dropdown"; 8 | import { MobileMenu } from "./mobile-menu"; 9 | import { useToggle } from "@uidotdev/usehooks"; 10 | 11 | export type NavigationLink = { 12 | label: string; 13 | href: string; 14 | current: boolean; 15 | }; 16 | 17 | export default function Authenticated({ children }: PropsWithChildren) { 18 | const links: NavigationLink[] = [ 19 | { 20 | label: "Dashboard", 21 | href: route("dashboard"), 22 | current: route().current("dashboard"), 23 | }, 24 | ]; 25 | 26 | const user = usePage().props.auth.user; 27 | const [isDropdownOpen, toggleDropdown] = useToggle(false); 28 | 29 | return ( 30 |
31 | 72 | 73 |
74 | {children} 75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /inertia/components/layouts/guest-layout.tsx: -------------------------------------------------------------------------------- 1 | import ApplicationLogo from "~/components/application-logo"; 2 | import { Card, CardContent } from "~/components/ui/card"; 3 | import { Link } from "@inertiajs/react"; 4 | import { PropsWithChildren } from "react"; 5 | 6 | export default function Guest({ children }: PropsWithChildren) { 7 | return ( 8 |
9 |
10 |
11 | 12 | 13 | 14 |
15 | 16 | 17 | {children} 18 | 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /inertia/components/layouts/mobile-menu.tsx: -------------------------------------------------------------------------------- 1 | import { InertiaLinkProps, Link, usePage } from "@inertiajs/react"; 2 | import { cn } from "~/lib/utils"; 3 | import { 4 | Drawer, 5 | DrawerContent, 6 | DrawerHeader, 7 | DrawerTitle, 8 | } from "~/components/ui/drawer"; 9 | import { NavigationLink } from "./authenticated-layout"; 10 | 11 | function MobileMenuButton({ 12 | children, 13 | className, 14 | current = false, 15 | ...props 16 | }: InertiaLinkProps & { current?: boolean }) { 17 | return ( 18 | 26 | {children} 27 | 28 | ); 29 | } 30 | 31 | type MobileMenuProps = { 32 | links: NavigationLink[]; 33 | toggle: () => void; 34 | show: boolean; 35 | }; 36 | 37 | export function MobileMenu({ links, toggle, show }: MobileMenuProps) { 38 | const user = usePage().props.auth.user; 39 | 40 | return ( 41 | 42 | 43 | 44 | Menu 45 | 46 |
47 |
48 | {links.map((link) => ( 49 | toggle()} 51 | key={link.href} 52 | href={link.href} 53 | current={link.current} 54 | > 55 | {link.label} 56 | 57 | ))} 58 |
59 | 60 |
61 |
62 |
63 | {user.name} 64 |
65 |
66 | {user.email} 67 |
68 |
69 | 70 |
71 | toggle()} 73 | href={route("profile.edit")} 74 | current={route().current("profile.edit")} 75 | > 76 | Profile 77 | 78 | toggle()} 80 | as="button" 81 | href={route("logout")} 82 | method="post" 83 | > 84 | Log Out 85 | 86 |
87 |
88 |
89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /inertia/components/layouts/navigation.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@inertiajs/react"; 2 | import { 3 | NavigationMenu, 4 | NavigationMenuItem, 5 | NavigationMenuList, 6 | } from "~/components/ui/navigation-menu"; 7 | import { cn } from "~/lib/utils"; 8 | import { NavigationLink } from "./authenticated-layout"; 9 | 10 | type Props = { 11 | links: NavigationLink[]; 12 | }; 13 | 14 | export function Navigation({ links }: Props) { 15 | return ( 16 | 17 | 18 | 19 | {links.map((link) => ( 20 | 28 | {link.label} 29 | 30 | ))} 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /inertia/components/layouts/user-dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@inertiajs/react"; 2 | import { Button } from "~/components/ui/button"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuTrigger, 8 | } from "~/components/ui/dropdown-menu"; 9 | import { ChevronDown, LogOut, User } from "lucide-react"; 10 | 11 | interface UserDropdownProps { 12 | user: { 13 | name: string; 14 | }; 15 | } 16 | 17 | export function UserDropdown({ user }: UserDropdownProps) { 18 | return ( 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | Profile 31 | 32 | 33 | 34 | 35 | 36 | Log Out 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /inertia/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { LoaderCircle } from "lucide-react"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 relative", 10 | { 11 | variants: { 12 | variant: { 13 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | loading?: boolean; 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ( 46 | { 47 | className, 48 | variant, 49 | size, 50 | asChild = false, 51 | loading = false, 52 | children, 53 | disabled, 54 | ...props 55 | }, 56 | ref, 57 | ) => { 58 | const Comp = asChild ? Slot : "button"; 59 | return ( 60 | 66 | 67 | {loading && ( 68 |
69 | 70 |
71 | )} 72 | {children} 73 |
74 |
75 | ); 76 | }, 77 | ); 78 | Button.displayName = "Button"; 79 | 80 | export { Button, buttonVariants }; 81 | -------------------------------------------------------------------------------- /inertia/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "~/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /inertia/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import { X } from "lucide-react"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const Dialog = DialogPrimitive.Root; 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger; 10 | 11 | const DialogPortal = DialogPrimitive.Portal; 12 | 13 | const DialogClose = DialogPrimitive.Close; 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )); 52 | DialogContent.displayName = DialogPrimitive.Content.displayName; 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ); 66 | DialogHeader.displayName = "DialogHeader"; 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ); 80 | DialogFooter.displayName = "DialogFooter"; 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )); 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )); 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogClose, 114 | DialogTrigger, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | }; 121 | -------------------------------------------------------------------------------- /inertia/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Drawer as DrawerPrimitive } from "vaul"; 3 | 4 | import { cn } from "~/lib/utils"; 5 | 6 | const Drawer = ({ 7 | shouldScaleBackground = true, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ); 15 | Drawer.displayName = "Drawer"; 16 | 17 | const DrawerTrigger = DrawerPrimitive.Trigger; 18 | 19 | const DrawerPortal = DrawerPrimitive.Portal; 20 | 21 | const DrawerClose = DrawerPrimitive.Close; 22 | 23 | const DrawerOverlay = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 34 | 35 | const DrawerContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, ...props }, ref) => ( 39 | 40 | 41 | 49 |
50 | {children} 51 | 52 | 53 | )); 54 | DrawerContent.displayName = "DrawerContent"; 55 | 56 | const DrawerHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
64 | ); 65 | DrawerHeader.displayName = "DrawerHeader"; 66 | 67 | const DrawerFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 |
75 | ); 76 | DrawerFooter.displayName = "DrawerFooter"; 77 | 78 | const DrawerTitle = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )); 91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 92 | 93 | const DrawerDescription = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )); 103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 104 | 105 | export { 106 | Drawer, 107 | DrawerPortal, 108 | DrawerOverlay, 109 | DrawerTrigger, 110 | DrawerClose, 111 | DrawerContent, 112 | DrawerHeader, 113 | DrawerFooter, 114 | DrawerTitle, 115 | DrawerDescription, 116 | }; 117 | -------------------------------------------------------------------------------- /inertia/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | 7 | import { cn } from "~/lib/utils"; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean; 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )); 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName; 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )); 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName; 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )); 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean; 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )); 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )); 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName; 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )); 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean; 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )); 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )); 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ); 181 | }; 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | }; 201 | -------------------------------------------------------------------------------- /inertia/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "~/lib/utils"; 4 | 5 | const Input = React.forwardRef< 6 | HTMLInputElement, 7 | React.ComponentProps<"input"> & { error?: string } 8 | >(({ className, type, error, ...props }, ref) => { 9 | return ( 10 | <> 11 | 22 | {error &&

{error}

} 23 | 24 | ); 25 | }); 26 | Input.displayName = "Input"; 27 | 28 | export { Input }; 29 | -------------------------------------------------------------------------------- /inertia/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /inertia/components/ui/navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"; 3 | import { cva } from "class-variance-authority"; 4 | import { ChevronDown } from "lucide-react"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | const NavigationMenu = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 20 | {children} 21 | 22 | 23 | )); 24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; 25 | 26 | const NavigationMenuList = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, ...props }, ref) => ( 30 | 38 | )); 39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; 40 | 41 | const NavigationMenuItem = NavigationMenuPrimitive.Item; 42 | 43 | const navigationMenuTriggerStyle = cva( 44 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50", 45 | ); 46 | 47 | const NavigationMenuTrigger = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, children, ...props }, ref) => ( 51 | 56 | {children}{" "} 57 | 62 | )); 63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; 64 | 65 | const NavigationMenuContent = React.forwardRef< 66 | React.ElementRef, 67 | React.ComponentPropsWithoutRef 68 | >(({ className, ...props }, ref) => ( 69 | 77 | )); 78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; 79 | 80 | const NavigationMenuLink = NavigationMenuPrimitive.Link; 81 | 82 | const NavigationMenuViewport = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 |
87 | 95 |
96 | )); 97 | NavigationMenuViewport.displayName = 98 | NavigationMenuPrimitive.Viewport.displayName; 99 | 100 | const NavigationMenuIndicator = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 112 |
113 | 114 | )); 115 | NavigationMenuIndicator.displayName = 116 | NavigationMenuPrimitive.Indicator.displayName; 117 | 118 | export { 119 | navigationMenuTriggerStyle, 120 | NavigationMenu, 121 | NavigationMenuList, 122 | NavigationMenuItem, 123 | NavigationMenuContent, 124 | NavigationMenuTrigger, 125 | NavigationMenuLink, 126 | NavigationMenuIndicator, 127 | NavigationMenuViewport, 128 | }; 129 | -------------------------------------------------------------------------------- /inertia/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 3 | 4 | import { cn } from "~/lib/utils"; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | Switch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { Switch }; 28 | -------------------------------------------------------------------------------- /inertia/components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ToastPrimitives from "@radix-ui/react-toast"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { X } from "lucide-react"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | const ToastProvider = ToastPrimitives.Provider; 9 | 10 | const ToastViewport = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 24 | 25 | const toastVariants = cva( 26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 27 | { 28 | variants: { 29 | variant: { 30 | default: "border bg-background text-foreground", 31 | destructive: 32 | "destructive group border-destructive bg-destructive text-destructive-foreground", 33 | }, 34 | }, 35 | defaultVariants: { 36 | variant: "default", 37 | }, 38 | }, 39 | ); 40 | 41 | const Toast = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef & 44 | VariantProps 45 | >(({ className, variant, ...props }, ref) => { 46 | return ( 47 | 52 | ); 53 | }); 54 | Toast.displayName = ToastPrimitives.Root.displayName; 55 | 56 | const ToastAction = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, ...props }, ref) => ( 60 | 68 | )); 69 | ToastAction.displayName = ToastPrimitives.Action.displayName; 70 | 71 | const ToastClose = React.forwardRef< 72 | React.ElementRef, 73 | React.ComponentPropsWithoutRef 74 | >(({ className, ...props }, ref) => ( 75 | 84 | 85 | 86 | )); 87 | ToastClose.displayName = ToastPrimitives.Close.displayName; 88 | 89 | const ToastTitle = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => ( 93 | 98 | )); 99 | ToastTitle.displayName = ToastPrimitives.Title.displayName; 100 | 101 | const ToastDescription = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )); 111 | ToastDescription.displayName = ToastPrimitives.Description.displayName; 112 | 113 | type ToastProps = React.ComponentPropsWithoutRef; 114 | 115 | type ToastActionElement = React.ReactElement; 116 | 117 | export { 118 | type ToastProps, 119 | type ToastActionElement, 120 | ToastProvider, 121 | ToastViewport, 122 | Toast, 123 | ToastTitle, 124 | ToastDescription, 125 | ToastClose, 126 | ToastAction, 127 | }; 128 | -------------------------------------------------------------------------------- /inertia/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from "~/hooks/use-toast"; 2 | import { 3 | Toast, 4 | ToastClose, 5 | ToastDescription, 6 | ToastProvider, 7 | ToastTitle, 8 | ToastViewport, 9 | } from "~/components/ui/toast"; 10 | 11 | export function Toaster() { 12 | const { toasts } = useToast(); 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
25 | {action} 26 | 27 |
28 | ); 29 | })} 30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /inertia/components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TogglePrimitive from "@radix-ui/react-toggle"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "~/lib/utils"; 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2", 11 | { 12 | variants: { 13 | variant: { 14 | default: "bg-transparent", 15 | outline: 16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", 17 | }, 18 | size: { 19 | default: "h-10 px-3 min-w-10", 20 | sm: "h-9 px-2.5 min-w-9", 21 | lg: "h-11 px-5 min-w-11", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | }, 29 | ); 30 | 31 | const Toggle = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef & 34 | VariantProps 35 | >(({ className, variant, size, ...props }, ref) => ( 36 | 41 | )); 42 | 43 | Toggle.displayName = TogglePrimitive.Root.displayName; 44 | 45 | export { Toggle, toggleVariants }; 46 | -------------------------------------------------------------------------------- /inertia/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --font-sans: "Inter", sans-serif; 8 | 9 | --background: 0 0% 100%; 10 | --foreground: 224 71.4% 4.1%; 11 | 12 | --muted: 220 14.3% 95.9%; 13 | --muted-foreground: 220 8.9% 46.1%; 14 | 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 224 71.4% 4.1%; 17 | 18 | --border: 220 13% 91%; 19 | --input: 220 13% 91%; 20 | 21 | --card: 0 0% 100%; 22 | --card-foreground: 224 71.4% 4.1%; 23 | 24 | --primary: 220.9 39.3% 11%; 25 | --primary-foreground: 210 20% 98%; 26 | 27 | --secondary: 220 14.3% 95.9%; 28 | --secondary-foreground: 220.9 39.3% 11%; 29 | 30 | --accent: 220 14.3% 95.9%; 31 | --accent-foreground: 220.9 39.3% 11%; 32 | 33 | --destructive: 0 84.2% 60.2%; 34 | --destructive-foreground: 210 20% 98%; 35 | 36 | --ring: 224 71.4% 4.1%; 37 | 38 | --radius: 0.5rem; 39 | 40 | --chart-1: 12 76% 61%; 41 | 42 | --chart-2: 173 58% 39%; 43 | 44 | --chart-3: 197 37% 24%; 45 | 46 | --chart-4: 43 74% 66%; 47 | 48 | --chart-5: 27 87% 67%; 49 | } 50 | 51 | .dark { 52 | --background: 224 71.4% 4.1%; 53 | --foreground: 210 20% 98%; 54 | 55 | --muted: 215 27.9% 16.9%; 56 | --muted-foreground: 217.9 10.6% 64.9%; 57 | 58 | --accent: 215 27.9% 16.9%; 59 | --accent-foreground: 210 20% 98%; 60 | 61 | --popover: 224 71.4% 4.1%; 62 | --popover-foreground: 210 20% 98%; 63 | 64 | --border: 215 27.9% 16.9%; 65 | --input: 215 27.9% 16.9%; 66 | 67 | --card: 224 71.4% 4.1%; 68 | --card-foreground: 210 20% 98%; 69 | 70 | --primary: 210 20% 98%; 71 | --primary-foreground: 220.9 39.3% 11%; 72 | 73 | --secondary: 215 27.9% 16.9%; 74 | --secondary-foreground: 210 20% 98%; 75 | 76 | --destructive: 0 62.8% 30.6%; 77 | --destructive-foreground: 210 20% 98%; 78 | 79 | --ring: 216 12.2% 83.9%; 80 | 81 | --radius: 0.5rem; 82 | 83 | --chart-1: 220 70% 50%; 84 | 85 | --chart-2: 160 60% 45%; 86 | 87 | --chart-3: 30 80% 55%; 88 | 89 | --chart-4: 280 65% 60%; 90 | 91 | --chart-5: 340 75% 55%; 92 | } 93 | } 94 | 95 | @layer base { 96 | * { 97 | @apply border-border; 98 | } 99 | body { 100 | @apply bg-background text-foreground; 101 | font-feature-settings: 102 | "rlig" 1, 103 | "calt" 1; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /inertia/hooks/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react"; 5 | 6 | import type { ToastActionElement, ToastProps } from "~/components/ui/toast"; 7 | 8 | const TOAST_LIMIT = 1; 9 | const TOAST_REMOVE_DELAY = 1000000; 10 | 11 | type ToasterToast = ToastProps & { 12 | id: string; 13 | title?: React.ReactNode; 14 | description?: React.ReactNode; 15 | action?: ToastActionElement; 16 | }; 17 | 18 | const actionTypes = { 19 | ADD_TOAST: "ADD_TOAST", 20 | UPDATE_TOAST: "UPDATE_TOAST", 21 | DISMISS_TOAST: "DISMISS_TOAST", 22 | REMOVE_TOAST: "REMOVE_TOAST", 23 | } as const; 24 | 25 | let count = 0; 26 | 27 | function genId() { 28 | count = (count + 1) % Number.MAX_SAFE_INTEGER; 29 | return count.toString(); 30 | } 31 | 32 | type ActionType = typeof actionTypes; 33 | 34 | type Action = 35 | | { 36 | type: ActionType["ADD_TOAST"]; 37 | toast: ToasterToast; 38 | } 39 | | { 40 | type: ActionType["UPDATE_TOAST"]; 41 | toast: Partial; 42 | } 43 | | { 44 | type: ActionType["DISMISS_TOAST"]; 45 | toastId?: ToasterToast["id"]; 46 | } 47 | | { 48 | type: ActionType["REMOVE_TOAST"]; 49 | toastId?: ToasterToast["id"]; 50 | }; 51 | 52 | interface State { 53 | toasts: ToasterToast[]; 54 | } 55 | 56 | const toastTimeouts = new Map>(); 57 | 58 | const addToRemoveQueue = (toastId: string) => { 59 | if (toastTimeouts.has(toastId)) { 60 | return; 61 | } 62 | 63 | const timeout = setTimeout(() => { 64 | toastTimeouts.delete(toastId); 65 | dispatch({ 66 | type: "REMOVE_TOAST", 67 | toastId: toastId, 68 | }); 69 | }, TOAST_REMOVE_DELAY); 70 | 71 | toastTimeouts.set(toastId, timeout); 72 | }; 73 | 74 | export const reducer = (state: State, action: Action): State => { 75 | switch (action.type) { 76 | case "ADD_TOAST": 77 | return { 78 | ...state, 79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 80 | }; 81 | 82 | case "UPDATE_TOAST": 83 | return { 84 | ...state, 85 | toasts: state.toasts.map((t) => 86 | t.id === action.toast.id ? { ...t, ...action.toast } : t, 87 | ), 88 | }; 89 | 90 | case "DISMISS_TOAST": { 91 | const { toastId } = action; 92 | 93 | // ! Side effects ! - This could be extracted into a dismissToast() action, 94 | // but I'll keep it here for simplicity 95 | if (toastId) { 96 | addToRemoveQueue(toastId); 97 | } else { 98 | state.toasts.forEach((toast) => { 99 | addToRemoveQueue(toast.id); 100 | }); 101 | } 102 | 103 | return { 104 | ...state, 105 | toasts: state.toasts.map((t) => 106 | t.id === toastId || toastId === undefined 107 | ? { 108 | ...t, 109 | open: false, 110 | } 111 | : t, 112 | ), 113 | }; 114 | } 115 | case "REMOVE_TOAST": 116 | if (action.toastId === undefined) { 117 | return { 118 | ...state, 119 | toasts: [], 120 | }; 121 | } 122 | return { 123 | ...state, 124 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 125 | }; 126 | } 127 | }; 128 | 129 | const listeners: Array<(state: State) => void> = []; 130 | 131 | let memoryState: State = { toasts: [] }; 132 | 133 | function dispatch(action: Action) { 134 | memoryState = reducer(memoryState, action); 135 | listeners.forEach((listener) => { 136 | listener(memoryState); 137 | }); 138 | } 139 | 140 | type Toast = Omit; 141 | 142 | function toast({ ...props }: Toast) { 143 | const id = genId(); 144 | 145 | const update = (props: ToasterToast) => 146 | dispatch({ 147 | type: "UPDATE_TOAST", 148 | toast: { ...props, id }, 149 | }); 150 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); 151 | 152 | dispatch({ 153 | type: "ADD_TOAST", 154 | toast: { 155 | ...props, 156 | id, 157 | open: true, 158 | onOpenChange: (open) => { 159 | if (!open) dismiss(); 160 | }, 161 | }, 162 | }); 163 | 164 | return { 165 | id: id, 166 | dismiss, 167 | update, 168 | }; 169 | } 170 | 171 | function useToast() { 172 | const [state, setState] = React.useState(memoryState); 173 | 174 | React.useEffect(() => { 175 | listeners.push(setState); 176 | return () => { 177 | const index = listeners.indexOf(setState); 178 | if (index > -1) { 179 | listeners.splice(index, 1); 180 | } 181 | }; 182 | }, [state]); 183 | 184 | return { 185 | ...state, 186 | toast, 187 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 188 | }; 189 | } 190 | 191 | export { useToast, toast }; 192 | -------------------------------------------------------------------------------- /inertia/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /inertia/pages/auth/confirm-password.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import { Input } from "~/components/ui/input"; 3 | import { Label } from "~/components/ui/label"; 4 | import GuestLayout from "~/components/layouts/guest-layout"; 5 | import { Head, useForm } from "@inertiajs/react"; 6 | import { FormEventHandler } from "react"; 7 | 8 | export default function ConfirmPassword() { 9 | const { data, setData, post, processing, errors, reset } = useForm({ 10 | password: "", 11 | }); 12 | 13 | const submit: FormEventHandler = (e) => { 14 | e.preventDefault(); 15 | 16 | post(route("password.confirm"), { 17 | onFinish: () => reset("password"), 18 | }); 19 | }; 20 | 21 | return ( 22 | 23 | 24 | 25 |
26 | This is a secure area of the application. Please confirm your password 27 | before continuing. 28 |
29 | 30 |
31 |
32 | 33 | setData("password", e.target.value)} 40 | error={errors.password} 41 | required 42 | autoFocus 43 | /> 44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /inertia/pages/auth/forgot-password.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import { Input } from "~/components/ui/input"; 3 | import GuestLayout from "~/components/layouts/guest-layout"; 4 | import { Head, useForm } from "@inertiajs/react"; 5 | import { FormEventHandler } from "react"; 6 | 7 | export default function ForgotPassword({ status }: { status?: string }) { 8 | const { data, setData, post, processing, errors } = useForm({ 9 | email: "", 10 | }); 11 | 12 | const submit: FormEventHandler = (e) => { 13 | e.preventDefault(); 14 | 15 | post(route("password.email")); 16 | }; 17 | 18 | return ( 19 | 20 | 21 | 22 |
23 | Forgot your password? No problem. Just let us know your email address 24 | and we will email you a password reset link that will allow you to 25 | choose a new one. 26 |
27 | 28 | {status && ( 29 |
{status}
30 | )} 31 | 32 |
33 | setData("email", e.target.value)} 40 | error={errors.email} 41 | autoFocus 42 | /> 43 | 44 |
45 | 48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /inertia/pages/auth/login.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import { Input } from "~/components/ui/input"; 3 | import { Label } from "~/components/ui/label"; 4 | import { Switch } from "~/components/ui/switch"; 5 | import GuestLayout from "~/components/layouts/guest-layout"; 6 | import { Head, Link, useForm } from "@inertiajs/react"; 7 | import { FormEventHandler } from "react"; 8 | 9 | export default function Login({ 10 | status, 11 | canResetPassword, 12 | }: { 13 | status?: string; 14 | canResetPassword: boolean; 15 | }) { 16 | const { data, setData, post, processing, errors, reset } = useForm({ 17 | email: "", 18 | password: "", 19 | remember: false, 20 | }); 21 | 22 | const submit: FormEventHandler = (e) => { 23 | e.preventDefault(); 24 | 25 | post(route("login"), { 26 | onFinish: () => reset("password"), 27 | }); 28 | }; 29 | 30 | return ( 31 | 32 | 33 | 34 | {status && ( 35 |
{status}
36 | )} 37 | 38 |
39 |
40 | 41 | 42 | setData("email", e.target.value)} 50 | error={errors.email} 51 | autoFocus 52 | /> 53 |
54 | 55 |
56 | 57 | 58 | setData("password", e.target.value)} 66 | error={errors.password} 67 | /> 68 |
69 | 70 |
71 | setData("remember", checked)} 75 | id="remember-me" 76 | /> 77 | 78 |
79 | 80 |
81 | {canResetPassword && ( 82 | 87 | )} 88 | 89 | 92 |
93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /inertia/pages/auth/register.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import { Input } from "~/components/ui/input"; 3 | import { Label } from "~/components/ui/label"; 4 | import GuestLayout from "~/components/layouts/guest-layout"; 5 | import { Head, Link, useForm } from "@inertiajs/react"; 6 | import { FormEventHandler } from "react"; 7 | 8 | export default function Register() { 9 | const { data, setData, post, processing, errors, reset } = useForm({ 10 | name: "", 11 | email: "", 12 | password: "", 13 | password_confirmation: "", 14 | }); 15 | 16 | const submit: FormEventHandler = (e) => { 17 | e.preventDefault(); 18 | 19 | post(route("register"), { 20 | onFinish: () => reset("password", "password_confirmation"), 21 | }); 22 | }; 23 | 24 | return ( 25 | 26 | 27 | 28 |
29 |
30 | 31 | setData("name", e.target.value)} 38 | error={errors.name} 39 | required 40 | /> 41 |
42 | 43 |
44 | 45 | setData("email", e.target.value)} 52 | error={errors.email} 53 | required 54 | /> 55 |
56 | 57 |
58 | 59 | setData("password", e.target.value)} 66 | error={errors.password} 67 | required 68 | /> 69 |
70 | 71 |
72 | 73 | setData("password_confirmation", e.target.value)} 80 | error={errors.password_confirmation} 81 | required 82 | /> 83 |
84 | 85 |
86 | 89 | 90 | 93 |
94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /inertia/pages/auth/reset-password.tsx: -------------------------------------------------------------------------------- 1 | import GuestLayout from "~/components/layouts/guest-layout"; 2 | import { Button } from "~/components/ui/button"; 3 | import { Input } from "~/components/ui/input"; 4 | import { Label } from "~/components/ui/label"; 5 | import { Head, useForm } from "@inertiajs/react"; 6 | import { FormEventHandler } from "react"; 7 | 8 | export default function ResetPassword({ 9 | token, 10 | email, 11 | }: { 12 | token: string; 13 | email: string; 14 | }) { 15 | const { data, setData, post, processing, errors, reset } = useForm({ 16 | token: token, 17 | email: email, 18 | password: "", 19 | password_confirmation: "", 20 | }); 21 | 22 | const submit: FormEventHandler = (e) => { 23 | e.preventDefault(); 24 | 25 | post(route("password.store"), { 26 | onFinish: () => reset("password", "password_confirmation"), 27 | }); 28 | }; 29 | 30 | return ( 31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | setData("email", e.target.value)} 46 | error={errors.email} 47 | /> 48 |
49 | 50 |
51 | 52 | setData("password", e.target.value)} 60 | error={errors.password} 61 | autoFocus 62 | /> 63 |
64 | 65 |
66 | 67 | 68 | setData("password_confirmation", e.target.value)} 75 | error={errors.password_confirmation} 76 | /> 77 |
78 | 79 |
80 | 83 |
84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /inertia/pages/auth/verify-email.tsx: -------------------------------------------------------------------------------- 1 | import GuestLayout from "~/components/layouts/guest-layout"; 2 | import { Button } from "~/components/ui/button"; 3 | import { Head, Link, useForm } from "@inertiajs/react"; 4 | import { FormEventHandler } from "react"; 5 | 6 | export default function VerifyEmail({ status }: { status?: string }) { 7 | const { post, processing } = useForm({}); 8 | 9 | const submit: FormEventHandler = (e) => { 10 | e.preventDefault(); 11 | 12 | post(route("verification.send")); 13 | }; 14 | 15 | return ( 16 | 17 | 18 | 19 |
20 | Thanks for signing up! Before getting started, could you verify your 21 | email address by clicking on the link we just emailed to you? If you 22 | didn't receive the email, we will gladly send you another. 23 |
24 | 25 | {status === "verification-link-sent" && ( 26 |
27 | A new verification link has been sent to the email address you 28 | provided during registration. 29 |
30 | )} 31 | 32 |
33 |
34 | 35 | 36 | 41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /inertia/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from "~/components/layouts/authenticated-layout"; 2 | import { Card, CardContent } from "~/components/ui/card"; 3 | import { Head } from "@inertiajs/react"; 4 | 5 | export default function Dashboard() { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 |
You're logged in!
13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /inertia/pages/profile/edit.tsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from "~/components/layouts/authenticated-layout"; 2 | import DeleteUserForm from "./partials/delete-user-form"; 3 | import UpdatePasswordForm from "./partials/update-password-form"; 4 | import UpdateProfileInformation from "./partials/update-profile-form"; 5 | import { Head } from "@inertiajs/react"; 6 | 7 | export default function Edit() { 8 | return ( 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /inertia/pages/profile/partials/delete-user-form.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import { Input } from "~/components/ui/input"; 3 | import { Label } from "~/components/ui/label"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogDescription, 10 | } from "~/components/ui/dialog"; 11 | import { useForm } from "@inertiajs/react"; 12 | import { FormEventHandler, useRef, useState } from "react"; 13 | import { cn } from "~/lib/utils"; 14 | import { 15 | Card, 16 | CardContent, 17 | CardDescription, 18 | CardHeader, 19 | CardTitle, 20 | } from "~/components/ui/card"; 21 | 22 | export default function DeleteUserForm() { 23 | const [confirmingUserDeletion, setConfirmingUserDeletion] = useState(false); 24 | const passwordInput = useRef(null); 25 | 26 | const { 27 | data, 28 | setData, 29 | delete: destroy, 30 | processing, 31 | reset, 32 | errors, 33 | clearErrors, 34 | } = useForm({ 35 | password: "", 36 | }); 37 | 38 | const confirmUserDeletion = () => { 39 | setConfirmingUserDeletion(true); 40 | }; 41 | 42 | const deleteUser: FormEventHandler = (e) => { 43 | e.preventDefault(); 44 | 45 | destroy(route("profile.destroy"), { 46 | preserveScroll: true, 47 | onSuccess: () => closeModal(), 48 | onError: () => passwordInput.current?.focus(), 49 | onFinish: () => reset(), 50 | }); 51 | }; 52 | 53 | const closeModal = () => { 54 | setConfirmingUserDeletion(false); 55 | 56 | clearErrors(); 57 | reset(); 58 | }; 59 | 60 | return ( 61 | 62 | 63 | Delete Account 64 | 65 | 66 | Once your account is deleted, all of its resources and data will be 67 | permanently deleted. Before deleting your account, please download any 68 | data or information that you wish to retain. 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | Are you sure you want to delete your account? 82 | 83 | 84 | Once your account is deleted, all of its resources and data will 85 | be permanently deleted. Please enter your password to confirm 86 | you would like to permanently delete your account. 87 | 88 | 89 | 90 |
91 |
92 | 95 | setData("password", e.target.value)} 102 | placeholder="Password" 103 | error={errors.password} 104 | autoFocus 105 | /> 106 |
107 | 108 |
109 | 112 | 119 |
120 |
121 |
122 |
123 |
124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /inertia/pages/profile/partials/update-password-form.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import { Input } from "~/components/ui/input"; 3 | import { Label } from "~/components/ui/label"; 4 | import { Transition } from "@headlessui/react"; 5 | import { useForm } from "@inertiajs/react"; 6 | import { FormEventHandler, useRef } from "react"; 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardHeader, 12 | CardTitle, 13 | } from "~/components/ui/card"; 14 | import { useToast } from "~/hooks/use-toast"; 15 | 16 | export default function UpdatePasswordForm() { 17 | const passwordInput = useRef(null); 18 | const currentPasswordInput = useRef(null); 19 | 20 | const { toast } = useToast(); 21 | const { data, setData, errors, put, reset, processing } = useForm({ 22 | current_password: "", 23 | password: "", 24 | password_confirmation: "", 25 | }); 26 | 27 | const updatePassword: FormEventHandler = (e) => { 28 | e.preventDefault(); 29 | 30 | put(route("password.update"), { 31 | preserveScroll: true, 32 | onSuccess: () => { 33 | reset(); 34 | toast({ 35 | title: "Password Updated", 36 | description: "Your password has been updated.", 37 | }); 38 | }, 39 | onError: (errors) => { 40 | if (errors.password) { 41 | reset("password", "password_confirmation"); 42 | passwordInput.current?.focus(); 43 | } 44 | 45 | if (errors.current_password) { 46 | reset("current_password"); 47 | currentPasswordInput.current?.focus(); 48 | } 49 | }, 50 | }); 51 | }; 52 | 53 | return ( 54 | 55 | 56 | Update Password 57 | 58 | 59 | Ensure your account is using a long, random password to stay secure. 60 | 61 | 62 | 63 | 64 |
65 |
66 | 67 | setData("current_password", e.target.value)} 74 | autoComplete="current-password" 75 | error={errors.current_password} 76 | /> 77 |
78 | 79 |
80 | 81 | setData("password", e.target.value)} 88 | autoComplete="new-password" 89 | error={errors.password} 90 | /> 91 |
92 | 93 |
94 | 95 | setData("password_confirmation", e.target.value)} 101 | autoComplete="new-password" 102 | error={errors.password_confirmation} 103 | /> 104 |
105 | 106 |
107 | 108 |
109 |
110 |
111 |
112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /inertia/pages/profile/partials/update-profile-form.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import { Input } from "~/components/ui/input"; 3 | import { Label } from "~/components/ui/label"; 4 | import { Transition } from "@headlessui/react"; 5 | import { Link, useForm, usePage } from "@inertiajs/react"; 6 | import { FormEventHandler } from "react"; 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardHeader, 12 | CardTitle, 13 | } from "~/components/ui/card"; 14 | import { PageProps } from "~/types"; 15 | import { useToast } from "~/hooks/use-toast"; 16 | 17 | export default function UpdateProfileInformation() { 18 | const { mustVerifyEmail, status, auth } = usePage< 19 | PageProps<{ 20 | mustVerifyEmail: boolean; 21 | status?: string; 22 | }> 23 | >().props; 24 | 25 | const { toast } = useToast(); 26 | const { data, setData, patch, errors, processing } = useForm({ 27 | name: auth.user.name, 28 | email: auth.user.email, 29 | }); 30 | 31 | const submit: FormEventHandler = (e) => { 32 | e.preventDefault(); 33 | 34 | patch(route("profile.update"), { 35 | onSuccess: () => { 36 | toast({ 37 | title: "Profile Updated", 38 | description: "Profile updated successfully.", 39 | }); 40 | }, 41 | }); 42 | }; 43 | 44 | return ( 45 | 46 | 47 | 48 | Profile Information 49 | 50 | 51 | 52 | Update your account's profile information and email address. 53 | 54 | 55 | 56 | 57 |
58 |
59 | 60 | setData("name", e.target.value)} 65 | required 66 | autoFocus 67 | autoComplete="name" 68 | error={errors.name} 69 | /> 70 |
71 | 72 |
73 | 74 | setData("email", e.target.value)} 80 | required 81 | autoComplete="username" 82 | error={errors.email} 83 | /> 84 |
85 | 86 | {mustVerifyEmail && auth.user.email_verified_at === null && ( 87 |
88 |

89 | Your email address is unverified. 90 | 95 |

96 | 97 | {status === "verification-link-sent" && ( 98 |
99 | A new verification link has been sent to your email address. 100 |
101 | )} 102 |
103 | )} 104 | 105 |
106 | 107 |
108 |
109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /inertia/ssr.tsx: -------------------------------------------------------------------------------- 1 | import { createInertiaApp } from "@inertiajs/react"; 2 | import createServer from "@inertiajs/react/server"; 3 | import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers"; 4 | import ReactDOMServer from "react-dom/server"; 5 | import { RouteName, route } from "ziggy-js"; 6 | 7 | const appName = import.meta.env.VITE_APP_NAME || "Laravel"; 8 | 9 | createServer((page) => 10 | createInertiaApp({ 11 | page, 12 | render: ReactDOMServer.renderToString, 13 | title: (title) => `${title} - ${appName}`, 14 | resolve: (name) => 15 | resolvePageComponent( 16 | `./pages/${name}.tsx`, 17 | import.meta.glob("./pages/**/*.tsx"), 18 | ), 19 | setup: ({ App, props }) => { 20 | /* eslint-disable */ 21 | // @ts-expect-error 22 | global.route = (name, params, absolute) => 23 | route(name, params as any, absolute, { 24 | ...page.props.ziggy, 25 | location: new URL(page.props.ziggy.location), 26 | }); 27 | /* eslint-enable */ 28 | 29 | return ; 30 | }, 31 | }), 32 | ); 33 | -------------------------------------------------------------------------------- /inertia/types/formRequests.ts: -------------------------------------------------------------------------------- 1 | export type ProfileUpdateRequest = { 2 | name: string; 3 | email: string; 4 | }; 5 | export type LoginRequest = { 6 | email: string; 7 | password: string; 8 | }; 9 | -------------------------------------------------------------------------------- /inertia/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import { PageProps as InertiaPageProps } from "@inertiajs/core"; 2 | import { AxiosInstance } from "axios"; 3 | import { Config } from "ziggy-js"; 4 | import { PageProps as AppPageProps } from "./"; 5 | import { RouteParams } from "./param"; 6 | import { CustomRouter } from "./route"; 7 | 8 | declare global { 9 | interface Window { 10 | axios: AxiosInstance; 11 | } 12 | 13 | var route: (() => CustomRouter) & 14 | (( 15 | name: T, 16 | params?: RouteParams[T], 17 | absolute?: boolean, 18 | config?: Config, 19 | ) => string); 20 | var Ziggy: ZiggyConfig; 21 | } 22 | 23 | declare module "@inertiajs/core" { 24 | interface PageProps extends InertiaPageProps, AppPageProps {} 25 | } 26 | -------------------------------------------------------------------------------- /inertia/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "ziggy-js"; 2 | import { User } from "~/types/model"; 3 | 4 | export type PageProps< 5 | T extends Record = Record, 6 | > = T & { 7 | auth: { 8 | user: User; 9 | }; 10 | ziggy: Config & { location: string }; 11 | }; 12 | -------------------------------------------------------------------------------- /inertia/types/model.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: number; 3 | name: string; 4 | email: string; 5 | email_verified_at?: string; 6 | created_at?: string; 7 | updated_at?: string; 8 | }; 9 | -------------------------------------------------------------------------------- /inertia/types/param.ts: -------------------------------------------------------------------------------- 1 | export type RouteParams = { 2 | "password.confirm": {}; 3 | dashboard: {}; 4 | "verification.send": {}; 5 | "password.request": {}; 6 | "password.email": {}; 7 | login: {}; 8 | logout: {}; 9 | "password.update": {}; 10 | "profile.edit": {}; 11 | "profile.update": {}; 12 | "profile.destroy": {}; 13 | register: {}; 14 | "password.store": {}; 15 | "password.reset": { 16 | token: string; 17 | }; 18 | "verification.notice": {}; 19 | "verification.verify": { 20 | id: string; 21 | hash: string; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /inertia/types/route.d.ts: -------------------------------------------------------------------------------- 1 | import type { Config, Router } from "ziggy-js"; 2 | import type { RouteParams } from "./param"; 3 | type CustomRouter = { 4 | get params(): RouteParams[T]; 5 | current(): Extract | undefined; 6 | current( 7 | name: Extract, 8 | params?: RouteParams[T], 9 | ): boolean; 10 | } & Router; 11 | declare global { 12 | declare function route(): CustomRouter; 13 | declare function route( 14 | name: T, 15 | params?: RouteParams[T], 16 | absolute?: boolean, 17 | config?: Config, 18 | ): string; 19 | } 20 | declare module "vue" { 21 | interface ComponentCustomProperties { 22 | route: (() => CustomRouter) & 23 | (( 24 | name: T, 25 | params?: RouteParams[T], 26 | absolute?: boolean, 27 | config?: Config, 28 | ) => string); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /inertia/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "tsc && vite build && vite build --ssr", 6 | "dev": "vite", 7 | "generate-types": "laravel-typegen -o inertia/types --laravel-enum --ziggy --form-request" 8 | }, 9 | "devDependencies": { 10 | "@7nohe/laravel-typegen": "^0.6.1", 11 | "@headlessui/react": "^2.0.0", 12 | "@inertiajs/react": "^1.0.0", 13 | "@tailwindcss/forms": "^0.5.3", 14 | "@types/node": "^18.13.0", 15 | "@types/react": "^18.0.28", 16 | "@types/react-dom": "^18.0.10", 17 | "@vitejs/plugin-react": "^4.2.0", 18 | "autoprefixer": "^10.4.12", 19 | "axios": "^1.7.4", 20 | "concurrently": "^9.0.1", 21 | "laravel-vite-plugin": "^1.0", 22 | "postcss": "^8.4.31", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "tailwindcss": "^3.2.1", 26 | "typescript": "^5.0.2", 27 | "vite": "^5.0", 28 | "ziggy-js": "2.4.1" 29 | }, 30 | "dependencies": { 31 | "@radix-ui/react-dialog": "^1.1.2", 32 | "@radix-ui/react-dropdown-menu": "^2.1.2", 33 | "@radix-ui/react-label": "^2.1.0", 34 | "@radix-ui/react-navigation-menu": "^1.2.1", 35 | "@radix-ui/react-slot": "^1.1.0", 36 | "@radix-ui/react-switch": "^1.1.1", 37 | "@radix-ui/react-toast": "^1.2.2", 38 | "@radix-ui/react-toggle": "^1.1.0", 39 | "@uidotdev/usehooks": "^2.4.1", 40 | "class-variance-authority": "^0.7.0", 41 | "clsx": "^2.1.1", 42 | "lucide-react": "^0.456.0", 43 | "tailwind-merge": "^2.5.4", 44 | "tailwindcss-animate": "^1.0.7", 45 | "vaul": "^1.1.1", 46 | "vite-plugin-watch": "^0.3.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obenchkroune/laravel-react-shadcn/fcbbd84c3b26bcc9d3da6a5764c2093c13dde01d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ config('app.name', 'Laravel') }} 8 | 9 | 10 | 11 | 12 | 13 | @routes 14 | @viteReactRefresh 15 | @vite(['inertia/app.tsx', "inertia/pages/{$page['component']}.tsx"]) 16 | @inertiaHead 17 | 18 | 19 | @inertia 20 | 21 | 22 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 15 | Route::get('register', [RegisteredUserController::class, 'create']) 16 | ->name('register'); 17 | 18 | Route::post('register', [RegisteredUserController::class, 'store']); 19 | 20 | Route::get('login', [AuthenticatedSessionController::class, 'create']) 21 | ->name('login'); 22 | 23 | Route::post('login', [AuthenticatedSessionController::class, 'store']); 24 | 25 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) 26 | ->name('password.request'); 27 | 28 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 29 | ->name('password.email'); 30 | 31 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) 32 | ->name('password.reset'); 33 | 34 | Route::post('reset-password', [NewPasswordController::class, 'store']) 35 | ->name('password.store'); 36 | }); 37 | 38 | Route::middleware('auth')->group(function () { 39 | Route::get('verify-email', EmailVerificationPromptController::class) 40 | ->name('verification.notice'); 41 | 42 | Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) 43 | ->middleware(['signed', 'throttle:6,1']) 44 | ->name('verification.verify'); 45 | 46 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 47 | ->middleware('throttle:6,1') 48 | ->name('verification.send'); 49 | 50 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) 51 | ->name('password.confirm'); 52 | 53 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); 54 | 55 | Route::put('password', [PasswordController::class, 'update'])->name('password.update'); 56 | 57 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 58 | ->name('logout'); 59 | }); 60 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote')->hourly(); 9 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | Route::has('login'), 11 | 'canRegister' => Route::has('register'), 12 | 'laravelVersion' => Application::VERSION, 13 | 'phpVersion' => PHP_VERSION, 14 | ]); 15 | }); 16 | 17 | Route::get('/dashboard', function () { 18 | return Inertia::render('dashboard'); 19 | })->middleware(['auth', 'verified'])->name('dashboard'); 20 | 21 | Route::middleware('auth')->group(function () { 22 | Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 23 | Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 24 | Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 25 | }); 26 | 27 | require __DIR__.'/auth.php'; 28 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | # install php dependencies 6 | composer install 7 | 8 | # install node dependencies 9 | echo "Select your preferred package manager:" 10 | PS3="Choose a package manager (1-4): " 11 | package_managers=("npm" "yarn" "pnpm" "bun") 12 | select pm in "${package_managers[@]}"; do 13 | case $pm in 14 | "npm") 15 | echo "Installing with npm..." 16 | npm install 17 | break 18 | ;; 19 | "yarn") 20 | echo "Installing with yarn..." 21 | yarn install 22 | break 23 | ;; 24 | "pnpm") 25 | echo "Installing with pnpm..." 26 | pnpm install 27 | break 28 | ;; 29 | "bun") 30 | echo "Installing with bun..." 31 | bun install 32 | break 33 | ;; 34 | *) 35 | echo "Invalid option $REPLY" 36 | ;; 37 | esac 38 | done 39 | 40 | # copy enirement file 41 | cp .env.example .env 42 | 43 | # generate app key 44 | php artisan key:generate 45 | 46 | # run migrations 47 | php artisan migrate 48 | 49 | prompt_yes_no() { 50 | while true; do 51 | read -p "$1 (y/n): " answer 52 | case $answer in 53 | [Yy]* ) return 0;; 54 | [Nn]* ) return 1;; 55 | * ) echo "Please answer y or n.";; 56 | esac 57 | done 58 | } 59 | 60 | # Generate IDE helper files 61 | if prompt_yes_no "Do you want to generate IDE helper files?"; then 62 | echo "Generating IDE helper files..." 63 | php artisan ide-helper:generate 64 | php artisan ide-helper:eloquent 65 | php artisan ide-helper:models --nowrite 66 | else 67 | echo "Skipping IDE helper files generation" 68 | fi 69 | 70 | # Ask to run composer dev 71 | if prompt_yes_no "Do you want to run composer dev script?"; then 72 | echo "Running composer dev script..." 73 | composer run-script dev 74 | else 75 | echo "Skipping composer dev script" 76 | fi 77 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/pail/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'tailwindcss'; 2 | import { fontFamily } from 'tailwindcss/defaultTheme'; 3 | import tailwindcssAnimate from 'tailwindcss-animate'; 4 | 5 | const config: Config = { 6 | darkMode: ['class'], 7 | content: [ 8 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 9 | './storage/framework/views/*.php', 10 | './resources/views/**/*.blade.php', 11 | './inertia/**/*.tsx', 12 | ], 13 | theme: { 14 | container: { 15 | center: true, 16 | padding: '2rem', 17 | screens: { 18 | '2xl': '1400px', 19 | }, 20 | }, 21 | extend: { 22 | colors: { 23 | border: 'hsl(var(--border))', 24 | input: 'hsl(var(--input))', 25 | ring: 'hsl(var(--ring))', 26 | background: 'hsl(var(--background))', 27 | foreground: 'hsl(var(--foreground))', 28 | primary: { 29 | DEFAULT: 'hsl(var(--primary))', 30 | foreground: 'hsl(var(--primary-foreground))', 31 | }, 32 | secondary: { 33 | DEFAULT: 'hsl(var(--secondary))', 34 | foreground: 'hsl(var(--secondary-foreground))', 35 | }, 36 | destructive: { 37 | DEFAULT: 'hsl(var(--destructive))', 38 | foreground: 'hsl(var(--destructive-foreground))', 39 | }, 40 | muted: { 41 | DEFAULT: 'hsl(var(--muted))', 42 | foreground: 'hsl(var(--muted-foreground))', 43 | }, 44 | accent: { 45 | DEFAULT: 'hsl(var(--accent))', 46 | foreground: 'hsl(var(--accent-foreground))', 47 | }, 48 | popover: { 49 | DEFAULT: 'hsl(var(--popover))', 50 | foreground: 'hsl(var(--popover-foreground))', 51 | }, 52 | card: { 53 | DEFAULT: 'hsl(var(--card))', 54 | foreground: 'hsl(var(--card-foreground))', 55 | }, 56 | chart: { 57 | '1': 'hsl(var(--chart-1))', 58 | '2': 'hsl(var(--chart-2))', 59 | '3': 'hsl(var(--chart-3))', 60 | '4': 'hsl(var(--chart-4))', 61 | '5': 'hsl(var(--chart-5))', 62 | }, 63 | }, 64 | borderRadius: { 65 | lg: 'var(--radius)', 66 | md: 'calc(var(--radius) - 2px)', 67 | sm: 'calc(var(--radius) - 4px)', 68 | }, 69 | fontFamily: { 70 | sans: ['var(--font-sans)', ...fontFamily.sans], 71 | }, 72 | keyframes: { 73 | 'accordion-down': { 74 | from: { 75 | height: '0', 76 | }, 77 | to: { 78 | height: 'var(--radix-accordion-content-height)', 79 | }, 80 | }, 81 | 'accordion-up': { 82 | from: { 83 | height: 'var(--radix-accordion-content-height)', 84 | }, 85 | to: { 86 | height: '0', 87 | }, 88 | }, 89 | }, 90 | animation: { 91 | 'accordion-down': 'accordion-down 0.2s ease-out', 92 | 'accordion-up': 'accordion-up 0.2s ease-out', 93 | }, 94 | }, 95 | }, 96 | plugins: [tailwindcssAnimate], 97 | }; 98 | 99 | export default config; 100 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('users can authenticate using the login screen', function () { 12 | $user = User::factory()->create(); 13 | 14 | $response = $this->post('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(route('dashboard', absolute: false)); 21 | }); 22 | 23 | test('users can not authenticate with invalid password', function () { 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/login', [ 27 | 'email' => $user->email, 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $this->assertGuest(); 32 | }); 33 | 34 | test('users can logout', function () { 35 | $user = User::factory()->create(); 36 | 37 | $response = $this->actingAs($user)->post('/logout'); 38 | 39 | $this->assertGuest(); 40 | $response->assertRedirect('/'); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | unverified()->create(); 10 | 11 | $response = $this->actingAs($user)->get('/verify-email'); 12 | 13 | $response->assertStatus(200); 14 | }); 15 | 16 | test('email can be verified', function () { 17 | $user = User::factory()->unverified()->create(); 18 | 19 | Event::fake(); 20 | 21 | $verificationUrl = URL::temporarySignedRoute( 22 | 'verification.verify', 23 | now()->addMinutes(60), 24 | ['id' => $user->id, 'hash' => sha1($user->email)] 25 | ); 26 | 27 | $response = $this->actingAs($user)->get($verificationUrl); 28 | 29 | Event::assertDispatched(Verified::class); 30 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 31 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 32 | }); 33 | 34 | test('email is not verified with invalid hash', function () { 35 | $user = User::factory()->unverified()->create(); 36 | 37 | $verificationUrl = URL::temporarySignedRoute( 38 | 'verification.verify', 39 | now()->addMinutes(60), 40 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 41 | ); 42 | 43 | $this->actingAs($user)->get($verificationUrl); 44 | 45 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this->actingAs($user)->get('/confirm-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('password can be confirmed', function () { 14 | $user = User::factory()->create(); 15 | 16 | $response = $this->actingAs($user)->post('/confirm-password', [ 17 | 'password' => 'password', 18 | ]); 19 | 20 | $response->assertRedirect(); 21 | $response->assertSessionHasNoErrors(); 22 | }); 23 | 24 | test('password is not confirmed with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $response = $this->actingAs($user)->post('/confirm-password', [ 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $response->assertSessionHasErrors(); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('reset password link can be requested', function () { 14 | Notification::fake(); 15 | 16 | $user = User::factory()->create(); 17 | 18 | $this->post('/forgot-password', ['email' => $user->email]); 19 | 20 | Notification::assertSentTo($user, ResetPassword::class); 21 | }); 22 | 23 | test('reset password screen can be rendered', function () { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create(); 27 | 28 | $this->post('/forgot-password', ['email' => $user->email]); 29 | 30 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 31 | $response = $this->get('/reset-password/'.$notification->token); 32 | 33 | $response->assertStatus(200); 34 | 35 | return true; 36 | }); 37 | }); 38 | 39 | test('password can be reset with valid token', function () { 40 | Notification::fake(); 41 | 42 | $user = User::factory()->create(); 43 | 44 | $this->post('/forgot-password', ['email' => $user->email]); 45 | 46 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 47 | $response = $this->post('/reset-password', [ 48 | 'token' => $notification->token, 49 | 'email' => $user->email, 50 | 'password' => 'password', 51 | 'password_confirmation' => 'password', 52 | ]); 53 | 54 | $response 55 | ->assertSessionHasNoErrors() 56 | ->assertRedirect(route('login')); 57 | 58 | return true; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 8 | 9 | $response = $this 10 | ->actingAs($user) 11 | ->from('/profile') 12 | ->put('/password', [ 13 | 'current_password' => 'password', 14 | 'password' => 'new-password', 15 | 'password_confirmation' => 'new-password', 16 | ]); 17 | 18 | $response 19 | ->assertSessionHasNoErrors() 20 | ->assertRedirect('/profile'); 21 | 22 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 23 | }); 24 | 25 | test('correct password must be provided to update password', function () { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->from('/profile') 31 | ->put('/password', [ 32 | 'current_password' => 'wrong-password', 33 | 'password' => 'new-password', 34 | 'password_confirmation' => 'new-password', 35 | ]); 36 | 37 | $response 38 | ->assertSessionHasErrors('current_password') 39 | ->assertRedirect('/profile'); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | 9 | test('new users can register', function () { 10 | $response = $this->post('/register', [ 11 | 'name' => 'Test User', 12 | 'email' => 'test@example.com', 13 | 'password' => 'password', 14 | 'password_confirmation' => 'password', 15 | ]); 16 | 17 | $this->assertAuthenticated(); 18 | $response->assertRedirect(route('dashboard', absolute: false)); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/Feature/ProfileTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this 9 | ->actingAs($user) 10 | ->get('/profile'); 11 | 12 | $response->assertOk(); 13 | }); 14 | 15 | test('profile information can be updated', function () { 16 | $user = User::factory()->create(); 17 | 18 | $response = $this 19 | ->actingAs($user) 20 | ->patch('/profile', [ 21 | 'name' => 'Test User', 22 | 'email' => 'test@example.com', 23 | ]); 24 | 25 | $response 26 | ->assertSessionHasNoErrors() 27 | ->assertRedirect('/profile'); 28 | 29 | $user->refresh(); 30 | 31 | $this->assertSame('Test User', $user->name); 32 | $this->assertSame('test@example.com', $user->email); 33 | $this->assertNull($user->email_verified_at); 34 | }); 35 | 36 | test('email verification status is unchanged when the email address is unchanged', function () { 37 | $user = User::factory()->create(); 38 | 39 | $response = $this 40 | ->actingAs($user) 41 | ->patch('/profile', [ 42 | 'name' => 'Test User', 43 | 'email' => $user->email, 44 | ]); 45 | 46 | $response 47 | ->assertSessionHasNoErrors() 48 | ->assertRedirect('/profile'); 49 | 50 | $this->assertNotNull($user->refresh()->email_verified_at); 51 | }); 52 | 53 | test('user can delete their account', function () { 54 | $user = User::factory()->create(); 55 | 56 | $response = $this 57 | ->actingAs($user) 58 | ->delete('/profile', [ 59 | 'password' => 'password', 60 | ]); 61 | 62 | $response 63 | ->assertSessionHasNoErrors() 64 | ->assertRedirect('/'); 65 | 66 | $this->assertGuest(); 67 | $this->assertNull($user->fresh()); 68 | }); 69 | 70 | test('correct password must be provided to delete account', function () { 71 | $user = User::factory()->create(); 72 | 73 | $response = $this 74 | ->actingAs($user) 75 | ->from('/profile') 76 | ->delete('/profile', [ 77 | 'password' => 'wrong-password', 78 | ]); 79 | 80 | $response 81 | ->assertSessionHasErrors('password') 82 | ->assertRedirect('/profile'); 83 | 84 | $this->assertNotNull($user->fresh()); 85 | }); 86 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "jsx": "react-jsx", 7 | "strict": true, 8 | "isolatedModules": true, 9 | "target": "ESNext", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "skipLibCheck": true, 13 | "noEmit": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["inertia/*"], 17 | "ziggy-js": ["vendor/tightenco/ziggy"] 18 | } 19 | }, 20 | "include": ["inertia/**/*.ts", "inertia/**/*.tsx", "inertia/**/*.d.ts", "inertia/app.tsx", "inertia/bootstrap.ts", "inertia/ssr.tsx"] 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | import path from 'path'; 5 | import { watch } from 'vite-plugin-watch'; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | watch({ 10 | pattern: [ 11 | 'app/Enums/**/*.php', 12 | 'app/Models/**/*.php', 13 | 'app/Http/Requests/**/*.php', 14 | 'routes/**/*.php' 15 | ], 16 | command: 'npm run generate-types', 17 | }), 18 | laravel({ 19 | input: 'inertia/app.tsx', 20 | ssr: 'inertia/ssr.tsx', 21 | refresh: true, 22 | }), 23 | react(), 24 | ], 25 | resolve: { 26 | alias: { 27 | '~': path.resolve(__dirname, './inertia'), 28 | }, 29 | }, 30 | }); 31 | --------------------------------------------------------------------------------