From a01f5142b9b4c3a19e2d0dbfb1090a9718debae3 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Mon, 24 Apr 2023 06:29:31 -0500 Subject: [PATCH] initial commit --- .editorconfig | 15 + .gitattributes | 19 + .gitignore | 4 + .styleci.yml | 8 + CHANGELOG.md | 1 + LICENSE.md | 21 ++ README.md | 143 ++++++++ UPGRADE.md | 3 + art/logo.svg | 11 + composer.json | 50 +++ phpstan.neon.dist | 5 + src/Console/InstallCommand.php | 213 +++++++++++ src/Console/InstallsReactorCommand.php | 112 ++++++ src/ReactorServiceProvider.php | 45 +++ stubs/inertia-react/.eslintrc.js | 45 +++ .../workflows/fix-php-code-formatting.yaml | 46 +++ stubs/inertia-react/.husky/pre-commit | 4 + stubs/inertia-react/.prettierrc | 8 + .../Auth/AuthenticatedSessionController.php | 53 +++ .../Auth/ConfirmablePasswordController.php | 42 +++ ...mailVerificationNotificationController.php | 25 ++ .../EmailVerificationPromptController.php | 23 ++ .../Auth/NewPasswordController.php | 69 ++++ .../Controllers/Auth/PasswordController.php | 29 ++ .../Auth/PasswordResetLinkController.php | 51 +++ .../Auth/RegisteredUserController.php | 52 +++ .../Auth/VerifyEmailController.php | 28 ++ .../Http/Controllers/ProfileController.php | 63 ++++ .../Http/Middleware/HandleInertiaRequests.php | 44 +++ .../app/Http/Requests/Auth/LoginRequest.php | 85 +++++ .../Http/Requests/ProfileUpdateRequest.php | 23 ++ stubs/inertia-react/jsconfig.json | 9 + stubs/inertia-react/package.json | 48 +++ .../Feature/Auth/PasswordUpdateTest.php | 40 ++ .../pest-tests/Feature/ProfileTest.php | 85 +++++ stubs/inertia-react/pest-tests/Pest.php | 48 +++ stubs/inertia-react/postcss.config.js | 6 + stubs/inertia-react/resources/css/app.css | 3 + .../resources/js/Components/Form/Checkbox.jsx | 72 ++++ .../resources/js/Components/Form/Label.jsx | 22 ++ .../js/Components/Form/TextInput.jsx | 63 ++++ .../resources/js/Components/Form/index.js | 3 + .../js/Components/Shared/ApplicationLogo.jsx | 15 + .../resources/js/Components/Shared/index.js | 1 + .../resources/js/Components/UI/Avatar.jsx | 35 ++ .../resources/js/Components/UI/Button.jsx | 113 ++++++ .../resources/js/Components/UI/Card.jsx | 67 ++++ .../resources/js/Components/UI/Dialog.jsx | 131 +++++++ .../resources/js/Components/UI/Dropdown.jsx | 123 +++++++ .../resources/js/Components/UI/Heading.jsx | 39 ++ .../resources/js/Components/UI/Spinner.jsx | 43 +++ .../resources/js/Components/UI/Text.jsx | 80 ++++ .../resources/js/Components/UI/index.js | 7 + .../resources/js/Hooks/useCurrentUser.jsx | 7 + .../resources/js/Layouts/Authenticated.jsx | 48 +++ .../resources/js/Layouts/Guest.jsx | 32 ++ .../js/Layouts/Partials/MainNavigation.jsx | 30 ++ .../js/Layouts/Partials/UserNavigation.jsx | 51 +++ .../inertia-react/resources/js/Models/User.js | 12 + .../js/Pages/Auth/ConfirmPassword.jsx | 64 ++++ .../js/Pages/Auth/ForgotPassword.jsx | 62 ++++ .../resources/js/Pages/Auth/Login.jsx | 97 +++++ .../resources/js/Pages/Auth/Register.jsx | 101 ++++++ .../resources/js/Pages/Auth/ResetPassword.jsx | 86 +++++ .../resources/js/Pages/Auth/VerifyEmail.jsx | 56 +++ .../resources/js/Pages/Dashboard.jsx | 13 + .../resources/js/Pages/Profile/Edit.jsx | 28 ++ .../Pages/Profile/Partials/DeleteUserForm.jsx | 86 +++++ .../Profile/Partials/UpdatePasswordForm.jsx | 88 +++++ .../Partials/UpdateProfileInformationForm.jsx | 86 +++++ .../resources/js/Pages/Welcome.jsx | 341 ++++++++++++++++++ stubs/inertia-react/resources/js/app.jsx | 25 ++ stubs/inertia-react/resources/js/bootstrap.js | 32 ++ .../resources/views/app.blade.php | 22 ++ .../resources/views/welcome.blade.php | 140 +++++++ stubs/inertia-react/routes/auth.php | 59 +++ stubs/inertia-react/routes/web.php | 38 ++ stubs/inertia-react/tailwind.config.js | 20 + stubs/inertia-react/vite.config.js | 13 + 79 files changed, 4030 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .styleci.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 UPGRADE.md create mode 100644 art/logo.svg create mode 100644 composer.json create mode 100644 phpstan.neon.dist create mode 100644 src/Console/InstallCommand.php create mode 100644 src/Console/InstallsReactorCommand.php create mode 100644 src/ReactorServiceProvider.php create mode 100644 stubs/inertia-react/.eslintrc.js create mode 100644 stubs/inertia-react/.github/workflows/fix-php-code-formatting.yaml create mode 100644 stubs/inertia-react/.husky/pre-commit create mode 100644 stubs/inertia-react/.prettierrc create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/AuthenticatedSessionController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/ConfirmablePasswordController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationNotificationController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationPromptController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/NewPasswordController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/PasswordController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/PasswordResetLinkController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/RegisteredUserController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/Auth/VerifyEmailController.php create mode 100644 stubs/inertia-react/app/Http/Controllers/ProfileController.php create mode 100644 stubs/inertia-react/app/Http/Middleware/HandleInertiaRequests.php create mode 100644 stubs/inertia-react/app/Http/Requests/Auth/LoginRequest.php create mode 100644 stubs/inertia-react/app/Http/Requests/ProfileUpdateRequest.php create mode 100644 stubs/inertia-react/jsconfig.json create mode 100644 stubs/inertia-react/package.json create mode 100644 stubs/inertia-react/pest-tests/Feature/Auth/PasswordUpdateTest.php create mode 100644 stubs/inertia-react/pest-tests/Feature/ProfileTest.php create mode 100644 stubs/inertia-react/pest-tests/Pest.php create mode 100644 stubs/inertia-react/postcss.config.js create mode 100644 stubs/inertia-react/resources/css/app.css create mode 100644 stubs/inertia-react/resources/js/Components/Form/Checkbox.jsx create mode 100644 stubs/inertia-react/resources/js/Components/Form/Label.jsx create mode 100644 stubs/inertia-react/resources/js/Components/Form/TextInput.jsx create mode 100644 stubs/inertia-react/resources/js/Components/Form/index.js create mode 100644 stubs/inertia-react/resources/js/Components/Shared/ApplicationLogo.jsx create mode 100644 stubs/inertia-react/resources/js/Components/Shared/index.js create mode 100644 stubs/inertia-react/resources/js/Components/UI/Avatar.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/Button.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/Card.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/Dialog.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/Dropdown.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/Heading.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/Spinner.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/Text.jsx create mode 100644 stubs/inertia-react/resources/js/Components/UI/index.js create mode 100644 stubs/inertia-react/resources/js/Hooks/useCurrentUser.jsx create mode 100644 stubs/inertia-react/resources/js/Layouts/Authenticated.jsx create mode 100644 stubs/inertia-react/resources/js/Layouts/Guest.jsx create mode 100644 stubs/inertia-react/resources/js/Layouts/Partials/MainNavigation.jsx create mode 100644 stubs/inertia-react/resources/js/Layouts/Partials/UserNavigation.jsx create mode 100644 stubs/inertia-react/resources/js/Models/User.js create mode 100644 stubs/inertia-react/resources/js/Pages/Auth/ConfirmPassword.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Auth/ForgotPassword.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Auth/Login.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Auth/Register.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Auth/ResetPassword.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Auth/VerifyEmail.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Dashboard.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Profile/Edit.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx create mode 100644 stubs/inertia-react/resources/js/Pages/Welcome.jsx create mode 100644 stubs/inertia-react/resources/js/app.jsx create mode 100644 stubs/inertia-react/resources/js/bootstrap.js create mode 100644 stubs/inertia-react/resources/views/app.blade.php create mode 100644 stubs/inertia-react/resources/views/welcome.blade.php create mode 100644 stubs/inertia-react/routes/auth.php create mode 100644 stubs/inertia-react/routes/web.php create mode 100644 stubs/inertia-react/tailwind.config.js create mode 100644 stubs/inertia-react/vite.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8c5adaa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +* text=auto + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +/art export-ignore +/tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.styleci.yml export-ignore +CHANGELOG.md export-ignore +phpstan.neon.dist export-ignore +phpunit.xml.dist export-ignore +UPGRADE.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..660fc15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.lock +/phpunit.xml +.phpunit.result.cache diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..e71ec89 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,8 @@ +php: + preset: laravel +js: + finder: + not-name: + - vite.config.js +css: true +vue: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..38bf0d7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Release Notes diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4bc3d25 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Lucky Media + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f5d509 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +

Logo Reactor for Laravel

+ +

+ + Total Downloads + + + Latest Stable Version + + + License + +

+ +# Reactor for Laravel + +Reactor is a Laravel package that provides minimal scaffolding for Laravel applications, making it simple for developers to get started with a fully functional web application. This package has a strong focus on frontend tooling and comes with Authentication, Inertia.js, React, and a very opinionated setup. Created by Lucky Media, Reactor aims to simplify and streamline your development process. + +## Why Reactor? +While Laravel Breeze provides a simple and minimal boilerplate with authentication for starting a new Laravel project, Reactor for Laravel is specifically designed to offer a more comprehensive set of features and tools catered towards modern web application. + +Reactor has a more advanced and opinionated frontend setup compared to Breeze. It includes pre-configured ESLint and Prettier configs, Husky hooks for automatic code formatting and linting, and a GitHub Workflow that leverages Laravel Pint. Together, these tools contribute to a clean and consistent codebase, fostering best practices among developers. + +Reactor comes with pre-built, opinionated frontend components that are influenced by `shadcn/ui`. These components are built with composability in mind, making it easier to build and customize application UI. + + +## Features + +- Authentication +- Pest for Laravel +- PHP Ide Helper +- Inertia.js with React +- Configured ESLint and Prettier +- Husky hooks to automatically format and lint code +- GitHub Workflow to automatically format code with Laravel Pint +- Opinionated frontend components with composability in mind, heavily inspired by `shadcn/ui` +- Radix UI headless components +- Sonner for Toast notifications +- Lucide React for icons + +## Installation + +Before you get started with Reactor for Laravel, make sure you have the following prerequisites: + +- Laravel 9+ +- Composer +- Node.js 16+ + +Follow these steps to install: + +1. Install the package via Composer: + +```bash +composer require lucky-media/reactor +``` + +2. Install the package by running + +```bash +php artisan reactor:install +``` + +3. Run the frontend build process: + +```bash +npm run dev +``` + +## Usage + +Reactor for Laravel comes with a pre-configured and opinionated setup for Laravel applications. This includes frontend components, headless components, and more. + +### Authentication + +The package comes with a ready-to-use authentication system. Make sure you've configured your database and run migrations. To customize your authentication views, you can either modify the included components or create your own. + +### Pest for Laravel +Reactor for Laravel comes with Pest for Laravel pre-installed. To run your tests, simply run: + +```bash +./vendor/bin/pest +``` + +### Inertia.js with React + +Reactor for Laravel is built on top of Inertia.js with React for a smooth, single-page application experience. Make sure to review the Inertia.js documentation to understand and wire up additional pages. + +### ESLint, Prettier, and Husky + +The package comes with pre-configured ESLint and Prettier configs, ensuring consistent and clean code. Additionally, Husky hooks run automatically to format and lint your code before committing. + +### PHP Ide Helper + +Integrate with popular IDEs to improve the developer experience. In the scripts section of your `composer.json` file, you need to add the following: +```json +{ + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force", + "@php artisan ide-helper:models -N", + "@php artisan ide-helper:generate", + "@php artisan ide-helper:eloquent", + "@php artisan ide-helper:meta" + ] +} +``` + +### GitHub Workflow with Laravel Pint + +Reactor for Laravel includes a pre-configured GitHub Workflow to automatically format code with Laravel Pint. This ensures a clean codebase and encourages best practices across your team. + +### Frontend Components + +The included frontend components are highly opinionated and influenced by [shadcn/ui](https://ui.shadcn.com/). They are designed for composability and ease of use. Be sure to explore and customize these components according to your needs. + +- Components are composed using [Tailwind Variants](https://www.tailwind-variants.org/). +- Radix UI headless components are used for greater flexibility. Read their docs [here](https://radix-ui.com/docs/primitives/overview/introduction). + +### Sonner, and Lucide React + +The package includes Sonner for Toast notifications, and Lucide React for icons. These packages are integrated to provide a uniform and highly customizable frontend experience. + +- [Sonner Documentation](https://sonner.emilkowal.ski/). +- [Lucide React Icons](https://lucide.dev/docs/lucide-react). + +## Support + +If you find any issues or have suggestions for improvements, please feel free to open an issue on the [GitHub repository](https://github.com/lucky-media/reactor). + + +## Security Vulnerabilities + +Please email us at [hello@luckymedia.dev](mailto:hello@luckymedia.dev) if you find any security vulnerabilities in Reactor. All security vulnerabilities will be promptly addressed. + +## Credits +- [Laravel Breeze](https://github.com/laravel/breeze) +- [Shadcn UI](https://github.com/shadcn/ui) +- [Radix UI](https://radix-ui.com/) +- [Sonner](https://sonner.emilkowal.ski/) +- [Lucide React](https://lucide.dev/docs/lucide-react) + +## License + +Reactor is open-sourced software licensed under the [MIT license](LICENSE.md). diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..a2ed7d5 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,3 @@ +# Upgrade Guide + +Future upgrade notes will be placed here. diff --git a/art/logo.svg b/art/logo.svg new file mode 100644 index 0000000..c8ac07d --- /dev/null +++ b/art/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b8bfb6a --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "lucky-media/reactor", + "description": "Minimal Laravel authentication scaffolding with React, Inertia and Tailwind CSS.", + "keywords": [ + "laravel", + "auth", + "react", + "inertia", + "tailwind" + ], + "license": "MIT", + "support": { + "issues": "https://github.com/lucky-media/reactor/issues", + "source": "https://github.com/lucky-media/reactor" + }, + "authors": [ + { + "name": "Lokman Musliu", + "email": "lokman@luckymedia.dev" + } + ], + "require": { + "php": "^8.1.0", + "illuminate/console": "^10.0", + "illuminate/filesystem": "^10.0", + "illuminate/support": "^10.0", + "illuminate/validation": "^10.0" + }, + "autoload": { + "psr-4": { + "LuckyMedia\\Reactor\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "LuckyMedia\\Reactor\\ReactorServiceProvider" + ] + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require-dev": { + "orchestra/testbench": "^8.0", + "phpstan/phpstan": "^1.10" + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..5b79b49 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + paths: + - src + + level: 0 diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php new file mode 100644 index 0000000..b5dad44 --- /dev/null +++ b/src/Console/InstallCommand.php @@ -0,0 +1,213 @@ +installInertiaReact(); + } + + + /** + * Install tests. + * + * @return bool + */ + protected function installTests() + { + (new Filesystem)->ensureDirectoryExists(base_path('tests/Feature')); + + + $this->removeComposerPackages(['phpunit/phpunit'], true); + + if (! $this->requireComposerPackages(['pestphp/pest:^2.0', 'pestphp/pest-plugin-laravel:^2.0'], true)) { + return false; + } + + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/pest-tests/Feature', base_path('tests/Feature')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/pest-tests/Unit', base_path('tests/Unit')); + (new Filesystem)->copy(__DIR__.'/../../stubs/inertia-react/pest-tests/Pest.php', base_path('tests/Pest.php')); + + return true; + } + + /** + * Install the middleware to a group in the application Http Kernel. + * + * @param string $after + * @param string $name + * @param string $group + * @return void + */ + protected function installMiddlewareAfter($after, $name, $group = 'web') + { + $httpKernel = file_get_contents(app_path('Http/Kernel.php')); + + $middlewareGroups = Str::before(Str::after($httpKernel, '$middlewareGroups = ['), '];'); + $middlewareGroup = Str::before(Str::after($middlewareGroups, "'$group' => ["), '],'); + + if (! Str::contains($middlewareGroup, $name)) { + $modifiedMiddlewareGroup = str_replace( + $after.',', + $after.','.PHP_EOL.' '.$name.',', + $middlewareGroup, + ); + + file_put_contents(app_path('Http/Kernel.php'), str_replace( + $middlewareGroups, + str_replace($middlewareGroup, $modifiedMiddlewareGroup, $middlewareGroups), + $httpKernel + )); + } + } + + /** + * Installs the given Composer Packages into the application. + * + * @param array $packages + * @param bool $asDev + * @return bool + */ + protected function requireComposerPackages(array $packages, $asDev = false) + { + $composer = $this->option('composer'); + + if ($composer !== 'global') { + $command = ['php', $composer, 'require']; + } + + $command = array_merge( + $command ?? ['composer', 'require'], + $packages, + $asDev ? ['--dev'] : [], + ); + + return (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) + ->setTimeout(null) + ->run(function ($type, $output) { + $this->output->write($output); + }) === 0; + } + + /** + * Removes the given Composer Packages from the application. + * + * @param array $packages + * @param bool $asDev + * @return bool + */ + protected function removeComposerPackages(array $packages, $asDev = false) + { + $composer = $this->option('composer'); + + if ($composer !== 'global') { + $command = ['php', $composer, 'remove']; + } + + $command = array_merge( + $command ?? ['composer', 'remove'], + $packages, + $asDev ? ['--dev'] : [], + ); + + return (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) + ->setTimeout(null) + ->run(function ($type, $output) { + $this->output->write($output); + }) === 0; + } + + + /** + * Delete the "node_modules" directory and remove the associated lock files. + * + * @return void + */ + protected static function flushNodeModules() + { + tap(new Filesystem, function ($files) { + $files->deleteDirectory(base_path('node_modules')); + + $files->delete(base_path('yarn.lock')); + $files->delete(base_path('package-lock.json')); + }); + } + + /** + * Replace a given string within a given file. + * + * @param string $search + * @param string $replace + * @param string $path + * @return void + */ + protected function replaceInFile($search, $replace, $path) + { + file_put_contents($path, str_replace($search, $replace, file_get_contents($path))); + } + + /** + * Get the path to the appropriate PHP binary. + * + * @return string + */ + protected function phpBinary() + { + return (new PhpExecutableFinder())->find(false) ?: 'php'; + } + + /** + * Run the given commands. + * + * @param array $commands + * @return void + */ + protected function runCommands($commands) + { + $process = Process::fromShellCommandline(implode(' && ', $commands), null, null, null, null); + + if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { + try { + $process->setTty(true); + } catch (RuntimeException $e) { + $this->output->writeln(' WARN '.$e->getMessage().PHP_EOL); + } + } + + $process->run(function ($type, $line) { + $this->output->write(' '.$line); + }); + } +} diff --git a/src/Console/InstallsReactorCommand.php b/src/Console/InstallsReactorCommand.php new file mode 100644 index 0000000..71a89a2 --- /dev/null +++ b/src/Console/InstallsReactorCommand.php @@ -0,0 +1,112 @@ +requireComposerPackages(['inertiajs/inertia-laravel:^0.6.3', 'laravel/sanctum:^3.2', 'tightenco/ziggy:^1.0'])) { + return 1; + } + + // Install Ide Helper + if (! $this->requireComposerPackages(['barryvdh/laravel-ide-helper:^2.13'], true)) { + return 1; + } + + // Copy Github workflow + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/.github', base_path('.github')); + + // Copy Husky hooks + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/.husky', base_path('.husky')); + + // NPM Packages... + copy(__DIR__.'/../../stubs/inertia-react/package.json', base_path('package.json')); + + // Controllers... + (new Filesystem)->ensureDirectoryExists(app_path('Http/Controllers')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/app/Http/Controllers', app_path('Http/Controllers')); + + // Requests... + (new Filesystem)->ensureDirectoryExists(app_path('Http/Requests')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/app/Http/Requests', app_path('Http/Requests')); + + // Middleware... + $this->installMiddlewareAfter('SubstituteBindings::class', '\App\Http\Middleware\HandleInertiaRequests::class'); + $this->installMiddlewareAfter('\App\Http\Middleware\HandleInertiaRequests::class', '\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class'); + + copy(__DIR__.'/../../stubs/inertia-react/app/Http/Middleware/HandleInertiaRequests.php', app_path('Http/Middleware/HandleInertiaRequests.php')); + + // Views... + copy(__DIR__.'/../../stubs/inertia-react/resources/views/app.blade.php', resource_path('views/app.blade.php')); + + // Components + Pages... + (new Filesystem)->ensureDirectoryExists(resource_path('js/Components')); + (new Filesystem)->ensureDirectoryExists(resource_path('js/Layouts')); + (new Filesystem)->ensureDirectoryExists(resource_path('js/Pages')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/resources/js/Hooks', resource_path('js/Hooks')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/resources/js/Models', resource_path('js/Models')); + + + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/resources/js/Components', resource_path('js/Components')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/resources/js/Layouts', resource_path('js/Layouts')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/resources/js/Pages', resource_path('js/Pages')); + + + // Tests... + if (! $this->installTests()) { + return 1; + } + + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-react/pest-tests/Feature', base_path('tests/Feature')); + + // Routes... + copy(__DIR__ . '/../../stubs/inertia-react/routes/web.php', base_path('routes/web.php')); + copy(__DIR__ . '/../../stubs/inertia-react/routes/auth.php', base_path('routes/auth.php')); + + // "Dashboard" Route... + $this->replaceInFile('/home', '/dashboard', app_path('Providers/RouteServiceProvider.php')); + + // Tailwind / Vite... + copy(__DIR__.'/../../stubs/inertia-react/resources/css/app.css', resource_path('css/app.css')); + copy(__DIR__.'/../../stubs/inertia-react/postcss.config.js', base_path('postcss.config.js')); + copy(__DIR__.'/../../stubs/inertia-react/tailwind.config.js', base_path('tailwind.config.js')); + copy(__DIR__.'/../../stubs/inertia-react/vite.config.js', base_path('vite.config.js')); + + copy(__DIR__ . '/../../stubs/inertia-react/jsconfig.json', base_path('jsconfig.json')); + copy(__DIR__.'/../../stubs/inertia-react/resources/js/app.jsx', resource_path('js/app.jsx')); + + // Eslint and Pretter config + copy(__DIR__.'/../../stubs/inertia-react/.eslintrc.js', base_path('.eslintrc.js')); + copy(__DIR__.'/../../stubs/inertia-react/.prettierrc', base_path('.prettierrc')); + + $this->replaceInFile('.vue', '.jsx', base_path('tailwind.config.js')); + + if (file_exists(resource_path('js/app.js'))) { + unlink(resource_path('js/app.js')); + } + + $this->components->info('Installing and building Node dependencies.'); + + if (file_exists(base_path('pnpm-lock.yaml'))) { + $this->runCommands(['pnpm install', 'pnpm run build']); + } elseif (file_exists(base_path('yarn.lock'))) { + $this->runCommands(['yarn install', 'yarn run build']); + } else { + $this->runCommands(['npm install', 'npm run build']); + } + + $this->line(''); + $this->components->info('Reactor scaffolding installed successfully.'); + } +} diff --git a/src/ReactorServiceProvider.php b/src/ReactorServiceProvider.php new file mode 100644 index 0000000..54704af --- /dev/null +++ b/src/ReactorServiceProvider.php @@ -0,0 +1,45 @@ +app->runningInConsole()) { + return; + } + + $this->commands([ + Console\InstallCommand::class, + ]); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return [Console\InstallCommand::class]; + } +} diff --git a/stubs/inertia-react/.eslintrc.js b/stubs/inertia-react/.eslintrc.js new file mode 100644 index 0000000..dc1d7cb --- /dev/null +++ b/stubs/inertia-react/.eslintrc.js @@ -0,0 +1,45 @@ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 2020, // Use the latest ecmascript standard + sourceType: 'module', // Allows using import/export statements + ecmaFeatures: { + jsx: true, // Enable JSX since we're using React + }, + }, + settings: { + react: { + version: 'detect', + }, + }, + env: { + browser: true, + amd: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:prettier/recommended', + ], + plugins: ['prettier', 'simple-import-sort'], + rules: { + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + 'react/jsx-first-prop-new-line': [2, 'multiline'], + 'react/jsx-max-props-per-line': [2, { maximum: 1, when: 'multiline' }], + 'react/jsx-indent-props': [2, 2], + 'react/jsx-closing-bracket-location': [2, 'tag-aligned'], + 'prettier/prettier': [ + 'error', + {}, + { + usePrettierrc: true, + }, + ], + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'no-console': 2, + }, +}; diff --git a/stubs/inertia-react/.github/workflows/fix-php-code-formatting.yaml b/stubs/inertia-react/.github/workflows/fix-php-code-formatting.yaml new file mode 100644 index 0000000..5b126b0 --- /dev/null +++ b/stubs/inertia-react/.github/workflows/fix-php-code-formatting.yaml @@ -0,0 +1,46 @@ +name: Laravel Pint + +on: + pull_request: + +jobs: + php-code-styling: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + + - name: Fix PHP code style issues + uses: aglipanci/laravel-pint-action@1.0.0 + + - name: Cache node modules + id: cache-npm + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + name: List the state of node modules + continue-on-error: true + run: npm list + + - name: Install dependencies + run: npm ci + - name: Format code + run: npm run format && npm run lint + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fix styling diff --git a/stubs/inertia-react/.husky/pre-commit b/stubs/inertia-react/.husky/pre-commit new file mode 100644 index 0000000..14ccce4 --- /dev/null +++ b/stubs/inertia-react/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm run pre-commit \ No newline at end of file diff --git a/stubs/inertia-react/.prettierrc b/stubs/inertia-react/.prettierrc new file mode 100644 index 0000000..0f775c1 --- /dev/null +++ b/stubs/inertia-react/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 80, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "arrowParens": "avoid" +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/stubs/inertia-react/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..fedd3ce --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,53 @@ + Route::has('password.request'), + 'status' => session('status'), + ]); + } + + /** + * Handle an incoming authentication request. + */ + public function store(LoginRequest $request): RedirectResponse + { + $request->authenticate(); + + $request->session()->regenerate(); + + return redirect()->intended(RouteServiceProvider::HOME); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): RedirectResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/stubs/inertia-react/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 0000000..1031482 --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,42 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(RouteServiceProvider::HOME); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..96ba772 --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,25 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME); + } + + $request->user()->sendEmailVerificationNotification(); + + return back()->with('status', 'verification-link-sent'); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationPromptController.php new file mode 100644 index 0000000..813b6ed --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -0,0 +1,23 @@ +user()->hasVerifiedEmail() + ? redirect()->intended(RouteServiceProvider::HOME) + : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/NewPasswordController.php b/stubs/inertia-react/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..394cc4a --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,69 @@ + $request->email, + 'token' => $request->route('token'), + ]); + } + + /** + * Handle an incoming new password request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + if ($status == Password::PASSWORD_RESET) { + return redirect()->route('login')->with('status', __($status)); + } + + throw ValidationException::withMessages([ + 'email' => [trans($status)], + ]); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/PasswordController.php b/stubs/inertia-react/app/Http/Controllers/Auth/PasswordController.php new file mode 100644 index 0000000..57a82b5 --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/PasswordController.php @@ -0,0 +1,29 @@ +validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back(); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/PasswordResetLinkController.php b/stubs/inertia-react/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..b22c544 --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,51 @@ + session('status'), + ]); + } + + /** + * Handle an incoming password reset link request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'email' => 'required|email', + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + if ($status == Password::RESET_LINK_SENT) { + return back()->with('status', __($status)); + } + + throw ValidationException::withMessages([ + 'email' => [trans($status)], + ]); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/RegisteredUserController.php b/stubs/inertia-react/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..a9f20f2 --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,52 @@ +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:'.User::class, + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + ]); + + event(new Registered($user)); + + Auth::login($user); + + return redirect(RouteServiceProvider::HOME); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/Auth/VerifyEmailController.php b/stubs/inertia-react/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..ea87940 --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,28 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } +} diff --git a/stubs/inertia-react/app/Http/Controllers/ProfileController.php b/stubs/inertia-react/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..3c33399 --- /dev/null +++ b/stubs/inertia-react/app/Http/Controllers/ProfileController.php @@ -0,0 +1,63 @@ + $request->user() instanceof MustVerifyEmail, + 'status' => session('status'), + ]); + } + + /** + * Update the user's profile information. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $request->user()->fill($request->validated()); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return Redirect::route('profile.edit'); + } + + /** + * Delete the user's account. + */ + public function destroy(Request $request): RedirectResponse + { + $request->validate([ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return Redirect::to('/'); + } +} diff --git a/stubs/inertia-react/app/Http/Middleware/HandleInertiaRequests.php b/stubs/inertia-react/app/Http/Middleware/HandleInertiaRequests.php new file mode 100644 index 0000000..73266e2 --- /dev/null +++ b/stubs/inertia-react/app/Http/Middleware/HandleInertiaRequests.php @@ -0,0 +1,44 @@ + + */ + public function share(Request $request): array + { + return array_merge(parent::share($request), [ + 'auth' => [ + 'user' => $request->user(), + ], + 'ziggy' => function () use ($request) { + return array_merge((new Ziggy)->toArray(), [ + 'location' => $request->url(), + ]); + }, + ]); + } +} diff --git a/stubs/inertia-react/app/Http/Requests/Auth/LoginRequest.php b/stubs/inertia-react/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..7a19bc0 --- /dev/null +++ b/stubs/inertia-react/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,85 @@ + + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); + } +} diff --git a/stubs/inertia-react/app/Http/Requests/ProfileUpdateRequest.php b/stubs/inertia-react/app/Http/Requests/ProfileUpdateRequest.php new file mode 100644 index 0000000..327ce6f --- /dev/null +++ b/stubs/inertia-react/app/Http/Requests/ProfileUpdateRequest.php @@ -0,0 +1,23 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['string', 'max:255'], + 'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], + ]; + } +} diff --git a/stubs/inertia-react/jsconfig.json b/stubs/inertia-react/jsconfig.json new file mode 100644 index 0000000..e03f793 --- /dev/null +++ b/stubs/inertia-react/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": "../inertia-common", + "paths": { + "@/*": ["resources/js/*"] + } + }, + "exclude": ["node_modules", "public"] +} diff --git a/stubs/inertia-react/package.json b/stubs/inertia-react/package.json new file mode 100644 index 0000000..543e8b9 --- /dev/null +++ b/stubs/inertia-react/package.json @@ -0,0 +1,48 @@ +{ + "private": true, + "scripts": { + "prepare": "husky install", + "dev": "vite", + "build": "vite build", + "lint": "eslint --fix .", + "format": "prettier 'resources/{js,jsx}/**/*.{js,jsx}' --write", + "pre-commit": "npm run format && lint-staged && pretty-quick --staged" + }, + "devDependencies": { + "@inertiajs/react": "^1.0.3", + "@radix-ui/react-checkbox": "^1.0.3", + "@radix-ui/react-label": "^2.0.1", + "@radix-ui/react-dialog": "^1.0.3", + "@radix-ui/react-dropdown-menu": "^2.0.4", + "@tailwindcss/forms": "^0.5.3", + "autoprefixer": "^10.4.14", + "axios": "^1.3.5", + "eslint": "^8.38.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-simple-import-sort": "^10.0.0", + "husky": "^8.0.3", + "laravel-vite-plugin": "^0.7.4", + "lint-staged": "^13.2.1", + "postcss": "^8.4.21", + "prettier": "^2.8.7", + "prettier-plugin-tailwindcss": "^0.2.7", + "pretty-quick": "^3.1.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sonner": "^0.3.0", + "tailwindcss": "^3.3.1", + "vite": "^4.2.1", + "@vitejs/plugin-react": "^3.1.0", + "lucide-react": "^0.170.0" + }, + "dependencies": { + "clsx": "^1.2.1", + "tailwind-variants": "^0.1.2" + }, + "lint-staged": { + "*.js": "eslint --cache --fix" + } +} diff --git a/stubs/inertia-react/pest-tests/Feature/Auth/PasswordUpdateTest.php b/stubs/inertia-react/pest-tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..96929b2 --- /dev/null +++ b/stubs/inertia-react/pest-tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,40 @@ +create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); +}); + +test('correct password must be provided to update password', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrors('current_password') + ->assertRedirect('/profile'); +}); diff --git a/stubs/inertia-react/pest-tests/Feature/ProfileTest.php b/stubs/inertia-react/pest-tests/Feature/ProfileTest.php new file mode 100644 index 0000000..a6a1721 --- /dev/null +++ b/stubs/inertia-react/pest-tests/Feature/ProfileTest.php @@ -0,0 +1,85 @@ +create(); + + $response = $this + ->actingAs($user) + ->get('/profile'); + + $response->assertOk(); +}); + +test('profile information can be updated', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); +}); + +test('email verification status is unchanged when the email address is unchanged', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => $user->email, + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertNotNull($user->refresh()->email_verified_at); +}); + +test('user can delete their account', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->delete('/profile', [ + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); +}); + +test('correct password must be provided to delete account', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->delete('/profile', [ + 'password' => 'wrong-password', + ]); + + $response + ->assertSessionHasErrors('password') + ->assertRedirect('/profile'); + + $this->assertNotNull($user->fresh()); +}); diff --git a/stubs/inertia-react/pest-tests/Pest.php b/stubs/inertia-react/pest-tests/Pest.php new file mode 100644 index 0000000..e2eb380 --- /dev/null +++ b/stubs/inertia-react/pest-tests/Pest.php @@ -0,0 +1,48 @@ +in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/stubs/inertia-react/postcss.config.js b/stubs/inertia-react/postcss.config.js new file mode 100644 index 0000000..67cdf1a --- /dev/null +++ b/stubs/inertia-react/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/stubs/inertia-react/resources/css/app.css b/stubs/inertia-react/resources/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/stubs/inertia-react/resources/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/stubs/inertia-react/resources/js/Components/Form/Checkbox.jsx b/stubs/inertia-react/resources/js/Components/Form/Checkbox.jsx new file mode 100644 index 0000000..41e78fe --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/Form/Checkbox.jsx @@ -0,0 +1,72 @@ +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { Check } from 'lucide-react'; +import { forwardRef } from 'react'; +import { tv } from 'tailwind-variants'; + +import Label from './Label'; + +const CheckStyles = tv({ + base: [ + 'peer shrink-0 rounded bg-white', + 'border border-slate-300', + 'disabled:cursor-not-allowed disabled:opacity-50', + 'focus:outline-none focus:ring-2 focus:ring-slate-300 focus:ring-offset-2', + ], + variants: { + size: { + xs: 'w-4 h-4', + sm: 'w-5 h-5', + md: 'w-6 h-6', + }, + hasError: { + true: 'border-red-800', + }, + checked: { + true: 'bg-blue-600 text-white', + }, + }, + defaultVariants: { + size: 'sm', + }, +}); + +const IconStyles = tv({ + base: 'text-white', + variants: { + size: { + xs: 'h-3.5 w-3.5', + sm: 'w-4 h-4', + md: 'w-5 h-5', + }, + }, + defaultVariants: { + size: 'sm', + }, +}); + +const Checkbox = forwardRef(({ className, size, error, ...props }, ref) => ( + + + + + +)); + +const Group = ({ children }) => ( +
{children}
+); + +Checkbox.Group = Group; +Checkbox.Label = Label; +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export default Checkbox; diff --git a/stubs/inertia-react/resources/js/Components/Form/Label.jsx b/stubs/inertia-react/resources/js/Components/Form/Label.jsx new file mode 100644 index 0000000..f308e22 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/Form/Label.jsx @@ -0,0 +1,22 @@ +import * as LabelPrimitive from '@radix-ui/react-label'; +import { forwardRef } from 'react'; +import { tv } from 'tailwind-variants'; + +const labelVariants = tv({ + base: [ + 'text-sm font-medium leading-none', + 'peer-disabled:cursor-not-allowed peer-disabled:opacity-70', + ], +}); + +const Label = forwardRef(({ className, ...props }, ref) => ( + +)); + +Label.displayName = LabelPrimitive.Root.displayName; + +export default Label; diff --git a/stubs/inertia-react/resources/js/Components/Form/TextInput.jsx b/stubs/inertia-react/resources/js/Components/Form/TextInput.jsx new file mode 100644 index 0000000..94f71bc --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/Form/TextInput.jsx @@ -0,0 +1,63 @@ +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; +import { tv } from 'tailwind-variants'; + +import { Label } from './'; + +const InputStyles = tv({ + base: [ + 'flex h-10 w-full px-3 py-2 text-sm', + 'border border-slate-300 bg-transparent', + 'ring-offset-white focus:ring-slate-300 focus:border-slate-300', + 'focus-visible:outline-none focus-visible:ring-2', + 'focus-visible:ring-slate-300 focus-visible:ring-offset-2', + 'placeholder:text-slate-400', + 'file:border-0 file:bg-transparent file:text-sm file:font-medium', + 'disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-50', + ], + variants: { + radius: { + none: '', + sm: 'rounded-sm', + md: 'rounded-md', + lg: 'rounded-lg', + xl: 'rounded-xl', + }, + hasError: { + true: 'text-red-500 border-red-500', + }, + }, + defaultVariants: { + radius: 'xl', + }, +}); + +const TextInput = forwardRef( + ({ label, type = 'text', id, error, className, ...props }, ref) => { + return ( +
+ {label && ( + + )} + + {error && ( +

{error}

+ )} +
+ ); + } +); + +TextInput.displayName = TextInput; + +export default TextInput; diff --git a/stubs/inertia-react/resources/js/Components/Form/index.js b/stubs/inertia-react/resources/js/Components/Form/index.js new file mode 100644 index 0000000..874ec88 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/Form/index.js @@ -0,0 +1,3 @@ +export { default as Checkbox } from './Checkbox'; +export { default as Label } from './Label'; +export { default as TextInput } from './TextInput'; diff --git a/stubs/inertia-react/resources/js/Components/Shared/ApplicationLogo.jsx b/stubs/inertia-react/resources/js/Components/Shared/ApplicationLogo.jsx new file mode 100644 index 0000000..3c931d5 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/Shared/ApplicationLogo.jsx @@ -0,0 +1,15 @@ +import { clsx } from 'clsx'; + +// TODO: Add logo +export default function ApplicationLogo({ className }) { + return ( + + + + ); +} diff --git a/stubs/inertia-react/resources/js/Components/Shared/index.js b/stubs/inertia-react/resources/js/Components/Shared/index.js new file mode 100644 index 0000000..a8cf756 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/Shared/index.js @@ -0,0 +1 @@ +export { default as ApplicationLogo } from './ApplicationLogo'; diff --git a/stubs/inertia-react/resources/js/Components/UI/Avatar.jsx b/stubs/inertia-react/resources/js/Components/UI/Avatar.jsx new file mode 100644 index 0000000..0dff614 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Avatar.jsx @@ -0,0 +1,35 @@ +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; + +const Avatar = forwardRef(({ className, ...props }, ref) => ( +
+)); + +const AvatarName = forwardRef(({ className, ...props }, ref) => ( +
+)); + +AvatarName.displayName = 'AvatarName'; +Avatar.displayName = 'Avatar'; + +Avatar.Name = AvatarName; + +export default Avatar; diff --git a/stubs/inertia-react/resources/js/Components/UI/Button.jsx b/stubs/inertia-react/resources/js/Components/UI/Button.jsx new file mode 100644 index 0000000..df85bba --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Button.jsx @@ -0,0 +1,113 @@ +import { forwardRef } from 'react'; +import { tv } from 'tailwind-variants'; + +import Spinner from './Spinner'; + +const ButtonStyles = tv({ + base: [ + 'font-semibold', + 'focus:outline-none focus:ring-2 focus:ring-opacity-50', + 'transition ease-in-out duration-300', + ], + variants: { + variant: { + blue: 'bg-blue-600 hover:bg-blue-900 text-white focus:ring-blue-500', + red: 'bg-red-600 hover:bg-red-900 text-white focus:ring-red-500', + gray: 'bg-slate-200 text-slate-900 hover:bg-slate-900 hover:text-white focus:ring-slate-300', + ghost: 'bg-transparent focus:ring-slate-500', + }, + radius: { + xs: 'rounded-sm', + sm: 'rounded', + md: 'rounded-lg', + lg: 'rounded-2xl', + xl: 'rounded-[32px]', + full: 'rounded-full', + }, + size: { + none: 'h-10', + base: 'h-10 py-2 px-4', + sm: 'h-9 px-3', + lg: 'h-11 px-8', + }, + uppercase: { + true: 'uppercase', + }, + fullWidth: { + true: 'w-full text-center', + }, + disabled: { + true: [ + 'opacity-40', + 'cursor-not-allowed pointer-events-none', + 'border-transparent', + ], + }, + loading: { + true: [ + 'inline-flex justify-center items-center', + 'opacity-40 cursor-not-allowed pointer-events-none', + ], + }, + icon: { + true: 'inline-flex items-center', + }, + }, + defaultVariants: { + variant: 'blue', + radius: 'md', + size: 'base', + }, +}); + +const Button = forwardRef( + ( + { + size, + variant, + radius, + uppercase, + className, + disabled, + fullWidth, + loading, + children, + type, + icon, + iconPosition = 'end', + as, + ...props + }, + ref + ) => { + const Tag = as || 'button'; + return ( + + {loading && } + {icon && iconPosition === 'start' && icon} + {children} + {icon && iconPosition === 'end' && icon} + + ); + } +); + +Button.displayName = 'Button'; + +export default Button; diff --git a/stubs/inertia-react/resources/js/Components/UI/Card.jsx b/stubs/inertia-react/resources/js/Components/UI/Card.jsx new file mode 100644 index 0000000..75223ae --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Card.jsx @@ -0,0 +1,67 @@ +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; + +const Card = forwardRef(({ className, ...props }, ref) => ( +
+)); + +const CardHeader = forwardRef(({ className, ...props }, ref) => ( +
+)); + +const CardTitle = forwardRef(({ className, ...props }, ref) => ( +

+)); + +const CardDescription = forwardRef(({ className, ...props }, ref) => ( +

+)); + +const CardBody = forwardRef(({ className, ...props }, ref) => ( +

+)); + +const CardFooter = forwardRef(({ className, ...props }, ref) => ( +
+)); + +Card.displayName = 'Card'; +CardHeader.displayName = 'CardHeader'; +CardTitle.displayName = 'CardTitle'; +CardDescription.displayName = 'CardDescription'; +CardBody.displayName = 'CardBody'; +CardFooter.displayName = 'CardFooter'; + +Card.Header = CardHeader; +Card.Title = CardTitle; +Card.Description = CardDescription; +Card.Body = CardBody; +Card.Footer = CardFooter; + +export default Card; diff --git a/stubs/inertia-react/resources/js/Components/UI/Dialog.jsx b/stubs/inertia-react/resources/js/Components/UI/Dialog.jsx new file mode 100644 index 0000000..1af229f --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Dialog.jsx @@ -0,0 +1,131 @@ +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { clsx } from 'clsx'; +import { X } from 'lucide-react'; +import * as React from 'react'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = ({ className, children, ...props }) => ( + +
+ {children} +
+
+); + +const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( + +)); + +const DialogContent = React.forwardRef( + ({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + + ) +); + +const DialogHeader = ({ className, ...props }) => ( +
+); + +const DialogFooter = ({ className, ...props }) => ( +
+); + +const DialogTitle = React.forwardRef(({ className, ...props }, ref) => ( + +)); + +const DialogDescription = React.forwardRef(({ className, ...props }, ref) => ( + +)); + +DialogHeader.displayName = 'DialogHeader'; +DialogTitle.displayName = DialogPrimitive.Title.displayName; +DialogDescription.displayName = DialogPrimitive.Description.displayName; +DialogContent.displayName = DialogPrimitive.Content.displayName; +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; +DialogFooter.displayName = 'DialogFooter'; +DialogPortal.displayName = DialogPrimitive.Portal.displayName; + +Dialog.Header = DialogHeader; +Dialog.Title = DialogTitle; +Dialog.Description = DialogDescription; +Dialog.Content = DialogContent; +Dialog.Overlay = DialogOverlay; +Dialog.Footer = DialogFooter; +Dialog.Portal = DialogPortal; +Dialog.Trigger = DialogTrigger; +Dialog.Close = DialogPrimitive.Close; + +export default Dialog; diff --git a/stubs/inertia-react/resources/js/Components/UI/Dropdown.jsx b/stubs/inertia-react/resources/js/Components/UI/Dropdown.jsx new file mode 100644 index 0000000..3c0ea67 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Dropdown.jsx @@ -0,0 +1,123 @@ +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { clsx } from 'clsx'; +import { ChevronRight } from 'lucide-react'; +import { forwardRef } from 'react'; + +const Dropdown = DropdownMenuPrimitive.Root; +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; +const DropdownMenuGroup = DropdownMenuPrimitive.Group; +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuSubTrigger = forwardRef( + ({ className, inset, children, ...props }, ref) => ( + + {children} + + + ) +); + +const DropdownMenuSubContent = forwardRef(({ className, ...props }, ref) => ( + +)); + +const DropdownMenuContent = forwardRef( + ({ className, sideOffset = 4, ...props }, ref) => ( + + + + ) +); + +const DropdownMenuItem = forwardRef(({ className, inset, ...props }, ref) => ( + +)); + +const DropdownMenuLabel = forwardRef(({ className, inset, ...props }, ref) => ( + +)); + +const DropdownMenuSeparator = forwardRef(({ className, ...props }, ref) => ( + +)); + +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +Dropdown.Content = DropdownMenuContent; +Dropdown.Group = DropdownMenuGroup; +Dropdown.Item = DropdownMenuItem; +Dropdown.Label = DropdownMenuLabel; +Dropdown.Portal = DropdownMenuPortal; +Dropdown.Separator = DropdownMenuSeparator; +Dropdown.Sub = DropdownMenuSub; +Dropdown.SubContent = DropdownMenuSubContent; +Dropdown.SubTrigger = DropdownMenuSubTrigger; +Dropdown.Trigger = DropdownMenuTrigger; + +export default Dropdown; diff --git a/stubs/inertia-react/resources/js/Components/UI/Heading.jsx b/stubs/inertia-react/resources/js/Components/UI/Heading.jsx new file mode 100644 index 0000000..d61ad57 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Heading.jsx @@ -0,0 +1,39 @@ +import { tv } from 'tailwind-variants'; + +const HeadingStyles = tv({ + base: 'font-bold', + variants: { + size: { + 1: 'text-3xl leading-[1.3]', + 2: 'text-2xl leading-[1.35]', + 3: 'text-xl leading-[1.4]', + 4: 'text-lg leading-[1.45]', + 5: 'text-base leading-[1.5]', + 6: 'text-sm leading-[1.5]', + }, + color: { + gray: 'text-gray-900', + blue: 'text-blue-800', + red: 'text-red-800', + }, + }, + defaultVariants: { + color: 'gray', + size: 1, + }, +}); + +const Heading = ({ size, color, className, children, as, ...props }) => { + const Tag = as || 'h1'; + + return ( + + {children} + + ); +}; + +export default Heading; diff --git a/stubs/inertia-react/resources/js/Components/UI/Spinner.jsx b/stubs/inertia-react/resources/js/Components/UI/Spinner.jsx new file mode 100644 index 0000000..3a5f5ba --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Spinner.jsx @@ -0,0 +1,43 @@ +import { clsx } from 'clsx'; +import { tv } from 'tailwind-variants'; + +const SpinnerStyles = tv({ + base: ['inline animate-spin', 'fill-white text-white text-opacity-5'], + variants: { + size: { + xs: 'w-[18px] h-[18px]', + sm: 'w-6 h-6', + md: 'w-9 h-9', + lg: 'w-12 h-12', + xl: 'w-14 h-14', + }, + }, + defaultVariants: { + size: 'md', + }, +}); + +const Spinner = ({ size, className }) => { + return ( +
+ + + + + Loading... +
+ ); +}; + +export default Spinner; diff --git a/stubs/inertia-react/resources/js/Components/UI/Text.jsx b/stubs/inertia-react/resources/js/Components/UI/Text.jsx new file mode 100644 index 0000000..6ca913a --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/Text.jsx @@ -0,0 +1,80 @@ +import { tv } from 'tailwind-variants'; + +const TextStyles = tv({ + variants: { + size: { + xs: 'text-xs', + sm: 'text-sm', + base: 'text-base', + lg: 'text-lg', + xl: 'text-xl', + '2xl': 'text-2xl', + '3xl': 'text-3xl', + '4xl': 'text-4xl', + }, + color: { + primary: 'text-gray-900', + secondary: 'text-gray-600', + }, + tracking: { + tight: 'tracking-tight', + normal: 'tracking-normal', + wide: 'tracking-wide', + }, + weight: { + 100: 'font-thin', + 200: 'font-extralight', + 300: 'font-light', + 400: 'font-normal', + 500: 'font-medium', + 600: 'font-semibold', + 700: 'font-bold', + 800: 'font-extrabold', + 900: 'font-black', + }, + upper: { + true: 'uppercase', + }, + isLink: { + true: 'hover:underline text-blue-600', + }, + }, + defaultVariants: { + tracking: 'tight', + color: 'primary', + size: 'base', + weight: 500, + }, +}); + +const Text = ({ + size, + color, + weight, + tracking, + className, + children, + upper, + as, + ...props +}) => { + const Tag = as || 'p'; + return ( + + {children} + + ); +}; + +export default Text; diff --git a/stubs/inertia-react/resources/js/Components/UI/index.js b/stubs/inertia-react/resources/js/Components/UI/index.js new file mode 100644 index 0000000..06997e0 --- /dev/null +++ b/stubs/inertia-react/resources/js/Components/UI/index.js @@ -0,0 +1,7 @@ +export { default as Avatar } from './Avatar'; +export { default as Button } from './Button'; +export { default as Card } from './Card'; +export { default as Dialog } from './Dialog'; +export { default as Dropdown } from './Dropdown'; +export { default as Heading } from './Heading'; +export { default as Text } from './Text'; diff --git a/stubs/inertia-react/resources/js/Hooks/useCurrentUser.jsx b/stubs/inertia-react/resources/js/Hooks/useCurrentUser.jsx new file mode 100644 index 0000000..14dd988 --- /dev/null +++ b/stubs/inertia-react/resources/js/Hooks/useCurrentUser.jsx @@ -0,0 +1,7 @@ +import { usePage } from '@inertiajs/react'; + +import User from '@/Models/User'; + +export default function useCurrentUser() { + return new User(usePage().props.auth.user); +} diff --git a/stubs/inertia-react/resources/js/Layouts/Authenticated.jsx b/stubs/inertia-react/resources/js/Layouts/Authenticated.jsx new file mode 100644 index 0000000..0a177b9 --- /dev/null +++ b/stubs/inertia-react/resources/js/Layouts/Authenticated.jsx @@ -0,0 +1,48 @@ +import { usePage } from '@inertiajs/react'; +import { useEffect } from 'react'; +import { Toaster } from 'sonner'; +import { toast } from 'sonner'; + +import { Heading } from '@/Components/UI'; + +import MainNavigation from './Partials/MainNavigation'; +import UserNavigation from './Partials/UserNavigation'; + +export default function Authenticated({ children, title }) { + const { errors } = usePage().props; + + useEffect(() => { + if (Object.keys(errors).length === 0 && errors.constructor === Object) + return; + + Object.entries(errors).forEach(([, value]) => { + toast.error(value); + }); + }, [errors]); + + return ( + <> + +
+
+
+ +
+ {/* */} + +
+
+
+ {title && ( +
+
+ {title} +
+
+ )} + + {children} +
+ + ); +} diff --git a/stubs/inertia-react/resources/js/Layouts/Guest.jsx b/stubs/inertia-react/resources/js/Layouts/Guest.jsx new file mode 100644 index 0000000..97613e0 --- /dev/null +++ b/stubs/inertia-react/resources/js/Layouts/Guest.jsx @@ -0,0 +1,32 @@ +import { usePage } from '@inertiajs/react'; +import { useEffect } from 'react'; +import { Toaster } from 'sonner'; +import { toast } from 'sonner'; + +import { ApplicationLogo } from '@/Components/Shared'; + +export default function Guest({ children }) { + const { errors } = usePage().props; + + useEffect(() => { + if (Object.keys(errors).length === 0 && errors.constructor === Object) + return; + + Object.entries(errors).forEach(([, value]) => { + toast.error(value); + }); + }, [errors]); + + return ( + <> + +
+
+ + + {children} +
+
+ + ); +} diff --git a/stubs/inertia-react/resources/js/Layouts/Partials/MainNavigation.jsx b/stubs/inertia-react/resources/js/Layouts/Partials/MainNavigation.jsx new file mode 100644 index 0000000..32cce2e --- /dev/null +++ b/stubs/inertia-react/resources/js/Layouts/Partials/MainNavigation.jsx @@ -0,0 +1,30 @@ +import { Link } from '@inertiajs/react'; +import { clsx } from 'clsx'; + +import { ApplicationLogo } from '@/Components/Shared'; + +const MenuItem = ({ href, children }) => { + return ( + + {children} + + ); +}; + +export default function MainNavigation({ className, ...props }) { + return ( + + ); +} diff --git a/stubs/inertia-react/resources/js/Layouts/Partials/UserNavigation.jsx b/stubs/inertia-react/resources/js/Layouts/Partials/UserNavigation.jsx new file mode 100644 index 0000000..b1e43c1 --- /dev/null +++ b/stubs/inertia-react/resources/js/Layouts/Partials/UserNavigation.jsx @@ -0,0 +1,51 @@ +import { Link } from '@inertiajs/react'; +import { LogOut, User } from 'lucide-react'; + +import { Avatar, Button, Dropdown, Text } from '@/Components/UI'; +import useCurrentUser from '@/Hooks/useCurrentUser'; + +export default function UserNavigation() { + const { name, email, initials } = useCurrentUser(); + return ( + + + + + + +
+ {name} + + {email} + +
+
+ + + + + + Profile + + + + + + + + Logout + + +
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Models/User.js b/stubs/inertia-react/resources/js/Models/User.js new file mode 100644 index 0000000..fcba248 --- /dev/null +++ b/stubs/inertia-react/resources/js/Models/User.js @@ -0,0 +1,12 @@ +export default class User { + constructor(attributes = {}) { + Object.assign(this, attributes); + } + + get initials() { + return this.name + .split(' ') + .map(word => word[0]) + .join(''); + } +} diff --git a/stubs/inertia-react/resources/js/Pages/Auth/ConfirmPassword.jsx b/stubs/inertia-react/resources/js/Pages/Auth/ConfirmPassword.jsx new file mode 100644 index 0000000..31bc0e8 --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Auth/ConfirmPassword.jsx @@ -0,0 +1,64 @@ +import { Head, useForm } from '@inertiajs/react'; +import { useEffect } from 'react'; + +import { TextInput } from '@/Components/Form'; +import { Button, Card, Heading, Text } from '@/Components/UI'; +import Guest from '@/Layouts/Guest'; + +export default function ConfirmPassword() { + const { data, setData, post, processing, errors, reset } = useForm({ + password: '', + }); + + useEffect(() => { + return () => { + reset('password'); + }; + }, []); + + const submit = e => { + e.preventDefault(); + + post(route('password.confirm')); + }; + + return ( + + + + + This is a secure area of the application. Please confirm your password + before continuing. + + +
+ + + Confirm Password + + + setData('password', e.target.value)} + error={errors.password} + autoFocus + /> + + + + +
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Auth/ForgotPassword.jsx b/stubs/inertia-react/resources/js/Pages/Auth/ForgotPassword.jsx new file mode 100644 index 0000000..da505ed --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Auth/ForgotPassword.jsx @@ -0,0 +1,62 @@ +import { Head, useForm } from '@inertiajs/react'; + +import { TextInput } from '@/Components/Form'; +import { Button, Card, Heading, Text } from '@/Components/UI'; +import Guest from '@/Layouts/Guest'; + +export default function ForgotPassword({ status }) { + const { data, setData, post, processing, errors } = useForm({ + email: '', + }); + + const submit = e => { + e.preventDefault(); + + post(route('password.email')); + }; + + return ( + + + + {status && ( +
+ {status} +
+ )} + +
+ + + Forgot your password? + + No problem. Just let us know your email address and we will email + you a password reset link that will allow you to choose a new one. + + + + setData('email', e.target.value)} + placeholder="you@reactor.dev" + error={errors.email} + required + /> + + + +
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Auth/Login.jsx b/stubs/inertia-react/resources/js/Pages/Auth/Login.jsx new file mode 100644 index 0000000..ffe4dca --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Auth/Login.jsx @@ -0,0 +1,97 @@ +import { Head, Link, useForm } from '@inertiajs/react'; +import { useEffect } from 'react'; + +import { Checkbox, TextInput } from '@/Components/Form'; +import { Button, Card, Heading, Text } from '@/Components/UI'; +import Guest from '@/Layouts/Guest'; + +export default function Login({ status, canResetPassword }) { + const { data, setData, post, processing, errors, reset } = useForm({ + email: '', + password: '', + remember: false, + }); + + useEffect(() => { + return () => { + reset('password'); + }; + }, []); + + const submit = e => { + e.preventDefault(); + + post(route('login')); + }; + + return ( + + + + {status && ( +
{status}
+ )} + +
+ + + Welcome back! + + Do not have an account yet?{' '} + + Create account + + + + + setData('email', e.target.value)} + placeholder="you@reactor.dev" + error={errors.email} + value={data.email} + required + /> + setData('password', e.target.value)} + placeholder="Your password" + error={errors.password} + required + className="mt-4" + /> +
+ + setData('remember', e)} + /> + Remember me + + + {canResetPassword && ( + + Forgot password? + + )} +
+ +
+
+
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Auth/Register.jsx b/stubs/inertia-react/resources/js/Pages/Auth/Register.jsx new file mode 100644 index 0000000..1e1190c --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Auth/Register.jsx @@ -0,0 +1,101 @@ +import { Head, Link, useForm } from '@inertiajs/react'; +import { useEffect } from 'react'; + +import { TextInput } from '@/Components/Form'; +import { Button, Card, Heading, Text } from '@/Components/UI'; +import Guest from '@/Layouts/Guest'; + +export default function Register() { + const { data, setData, post, processing, errors, reset } = useForm({ + name: '', + email: '', + password: '', + password_confirmation: '', + }); + + useEffect(() => { + return () => { + reset('password', 'password_confirmation'); + }; + }, []); + + const submit = e => { + e.preventDefault(); + + post(route('register')); + }; + + return ( + + + +
+ + + Register + + Already have an account?{' '} + + Login + + + + +
+ setData('name', e.target.value)} + placeholder="Your name" + error={errors.name} + value={data.name} + required + /> + + setData('email', e.target.value)} + placeholder="you@reactor.dev" + error={errors.email} + value={data.email} + required + /> + setData('password', e.target.value)} + placeholder="Your password" + error={errors.password} + required + /> + + setData('password_confirmation', e.target.value)} + placeholder="Confirm Password" + error={errors.password_confirmation} + required + /> +
+ + +
+
+
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Auth/ResetPassword.jsx b/stubs/inertia-react/resources/js/Pages/Auth/ResetPassword.jsx new file mode 100644 index 0000000..120f381 --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Auth/ResetPassword.jsx @@ -0,0 +1,86 @@ +import { Head, useForm } from '@inertiajs/react'; +import { useEffect } from 'react'; + +import { TextInput } from '@/Components/Form'; +import { Button, Card, Heading } from '@/Components/UI'; +import Guest from '@/Layouts/Guest'; + +export default function ResetPassword({ token, email }) { + const { data, setData, post, processing, errors, reset } = useForm({ + token: token, + email: email, + password: '', + password_confirmation: '', + }); + + useEffect(() => { + return () => { + reset('password', 'password_confirmation'); + }; + }, []); + + const submit = e => { + e.preventDefault(); + + post(route('password.store')); + }; + + return ( + + + +
+ + + Reset Password + + +
+ setData('email', e.target.value)} + placeholder="you@reactor.dev" + error={errors.email} + value={data.email} + required + /> + setData('password', e.target.value)} + placeholder="Your password" + error={errors.password} + required + autoFocus + /> + + setData('password_confirmation', e.target.value)} + placeholder="Confirm Password" + error={errors.password_confirmation} + required + /> +
+ + +
+
+
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Auth/VerifyEmail.jsx b/stubs/inertia-react/resources/js/Pages/Auth/VerifyEmail.jsx new file mode 100644 index 0000000..75705cb --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Auth/VerifyEmail.jsx @@ -0,0 +1,56 @@ +import { Head, useForm } from '@inertiajs/react'; + +import { Button, Card, Heading, Text } from '@/Components/UI'; +import Guest from '@/Layouts/Guest'; + +export default function VerifyEmail({ status }) { + const { post, processing } = useForm({}); + + const submit = e => { + e.preventDefault(); + + post(route('verification.send')); + }; + + return ( + + + + + Thanks for signing up! Before getting started, could you verify your + email address by clicking on the link we just emailed to you? If you + didn't receive the email, we will gladly send you another. + + + {status === 'verification-link-sent' && ( + + A new verification link has been sent to the email address you + provided during registration. + + )} + +
+ + + Email Verification + + + + + + + + +
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Dashboard.jsx b/stubs/inertia-react/resources/js/Pages/Dashboard.jsx new file mode 100644 index 0000000..b74cc75 --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Dashboard.jsx @@ -0,0 +1,13 @@ +import { Head } from '@inertiajs/react'; + +import Authenticated from '@/Layouts/Authenticated'; + +export default function Dashboard() { + return ( + + + +
You're logged in!
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Profile/Edit.jsx b/stubs/inertia-react/resources/js/Pages/Profile/Edit.jsx new file mode 100644 index 0000000..b164181 --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Profile/Edit.jsx @@ -0,0 +1,28 @@ +import { Head } from '@inertiajs/react'; + +import Authenticated from '@/Layouts/Authenticated'; + +import DeleteUserForm from './Partials/DeleteUserForm'; +import UpdatePasswordForm from './Partials/UpdatePasswordForm'; +import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm'; + +export default function Edit({ mustVerifyEmail, status }) { + return ( + + + +
+
+ + + + + +
+
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx b/stubs/inertia-react/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx new file mode 100644 index 0000000..337a5a2 --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx @@ -0,0 +1,86 @@ +import { useForm } from '@inertiajs/react'; +import { useRef } from 'react'; + +import { TextInput } from '@/Components/Form'; +import { Button, Card, Dialog, Heading, Text } from '@/Components/UI'; + +export default function DeleteUserForm() { + const passwordInput = useRef(); + + const { + data, + setData, + delete: destroy, + processing, + reset, + errors, + } = useForm({ + password: '', + }); + + const deleteUser = e => { + e.preventDefault(); + + destroy(route('profile.destroy'), { + preserveScroll: true, + onError: () => passwordInput.current.focus(), + onFinish: () => reset(), + }); + }; + + return ( + + + Delete Account + + Once your account is deleted, all of its resources and data will be + permanently deleted.
Before deleting your account, please + download any data or information that you wish to retain. +
+
+ + + + + + + + + Are you sure you want to delete your account? + + + Once your account is deleted, all of its resources and data will + be permanently deleted. Please enter your password to confirm + you wouldlike to permanently delete your account. + + + +
+ setData('password', e.target.value)} + autoFocus + placeholder="Password" + error={errors.password} + /> + +
+ + + + + +
+ +
+
+
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx b/stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx new file mode 100644 index 0000000..604ae80 --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx @@ -0,0 +1,88 @@ +import { useForm } from '@inertiajs/react'; +import { useRef } from 'react'; +import { toast } from 'sonner'; + +import { TextInput } from '@/Components/Form'; +import { Button, Card, Heading, Text } from '@/Components/UI'; + +export default function UpdatePasswordForm() { + const passwordInput = useRef(); + const currentPasswordInput = useRef(); + + const { data, setData, errors, put, reset, processing } = useForm({ + current_password: '', + password: '', + password_confirmation: '', + }); + + const updatePassword = e => { + e.preventDefault(); + + put(route('password.update'), { + preserveScroll: true, + onSuccess: () => { + reset(); + toast.success('Password has been updated'); + }, + onError: errors => { + if (errors.password) { + reset('password', 'password_confirmation'); + passwordInput.current.focus(); + } + + if (errors.current_password) { + reset('current_password'); + currentPasswordInput.current.focus(); + } + }, + }); + }; + + return ( + + + Update Password + + Ensure your account is using a long, random password to stay secure. + + + +
+ setData('current_password', e.target.value)} + error={errors.current_password} + autoComplete="current-password" + /> + + setData('password', e.target.value)} + error={errors.password} + autoComplete="new-password" + /> + + setData('password_confirmation', e.target.value)} + error={errors.password_confirmation} + autoComplete="new-password" + /> + + + +
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx b/stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx new file mode 100644 index 0000000..33053ff --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx @@ -0,0 +1,86 @@ +import { Link, useForm } from '@inertiajs/react'; +import { toast } from 'sonner'; + +import { TextInput } from '@/Components/Form'; +import { Button, Card, Heading, Text } from '@/Components/UI'; +import useCurrentUser from '@/Hooks/useCurrentUser'; + +export default function UpdateProfileInformation({ mustVerifyEmail, status }) { + const { name, email, email_verified_at } = useCurrentUser(); + + const { data, setData, patch, errors, processing } = useForm({ + name, + email, + }); + + const submit = e => { + e.preventDefault(); + + patch(route('profile.update'), { + onSuccess: () => { + toast.success('Profile Information has been updated'); + }, + }); + }; + + return ( + + + Profile Information + + Update your account's profile information and email address. + + + +
+ setData('name', e.target.value)} + error={errors.name} + required + autoFocus + autoComplete="name" + /> + + setData('email', e.target.value)} + error={errors.email} + required + autoComplete="email" + /> + + {mustVerifyEmail && email_verified_at === null && ( + <> +
+ Your email address is unverified. + + + Re-send the verification email. + +
+ + {status === 'verification-link-sent' && ( + + A new verification link has been sent to your email address. + + )} + + )} + + + +
+
+ ); +} diff --git a/stubs/inertia-react/resources/js/Pages/Welcome.jsx b/stubs/inertia-react/resources/js/Pages/Welcome.jsx new file mode 100644 index 0000000..d182a44 --- /dev/null +++ b/stubs/inertia-react/resources/js/Pages/Welcome.jsx @@ -0,0 +1,341 @@ +import { Head, Link } from '@inertiajs/react'; + +export default function Welcome({ auth, laravelVersion, phpVersion }) { + return ( + <> + +
+
+ {auth.user ? ( + + Dashboard + + ) : ( + <> + + Log in + + + + Register + + + )} +
+ +
+
+ + + +
+ +
+
+ +
+
+ + + +
+ +

+ Documentation +

+ +

+ Laravel has wonderful documentation covering every aspect of + the framework. Whether you are a newcomer or have prior + experience with Laravel, we recommend reading our + documentation from beginning to end. +

+
+ + + + +
+ + +
+
+ + + +
+ +

+ Laracasts +

+ +

+ Laracasts offers thousands of video tutorials on Laravel, + PHP, and JavaScript development. Check them out, see for + yourself, and massively level up your development skills in + the process. +

+
+ + + + +
+ + +
+
+ + + +
+ +

+ Laravel News +

+ +

+ Laravel News is a community driven portal and newsletter + aggregating all of the latest and most important news in the + Laravel ecosystem, including new package releases and + tutorials. +

+
+ + + + +
+ +
+
+
+ + + +
+ +

+ Vibrant Ecosystem +

+ +

+ Laravel's robust library of first-party tools and libraries, + such as{' '} + + Forge + + ,{' '} + + Vapor + + ,{' '} + + Nova + + , and{' '} + + Envoyer + {' '} + help you take your projects to the next level. Pair them + with powerful open source libraries like{' '} + + Cashier + + ,{' '} + + Dusk + + ,{' '} + + Echo + + ,{' '} + + Horizon + + ,{' '} + + Sanctum + + ,{' '} + + Telescope + + , and more. +

+
+
+
+
+ +
+ + +
+ Laravel v{laravelVersion} (PHP v{phpVersion}) +
+
+
+
+ + + + ); +} diff --git a/stubs/inertia-react/resources/js/app.jsx b/stubs/inertia-react/resources/js/app.jsx new file mode 100644 index 0000000..80b97d0 --- /dev/null +++ b/stubs/inertia-react/resources/js/app.jsx @@ -0,0 +1,25 @@ +import '../css/app.css'; + +import { createInertiaApp } from '@inertiajs/react'; +import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; +import { createRoot } from 'react-dom/client'; + +const appName = + window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel'; + +createInertiaApp({ + title: title => `${title} - ${appName}`, + resolve: name => + resolvePageComponent( + `./Pages/${name}.jsx`, + import.meta.glob('./Pages/**/*.jsx') + ), + setup({ el, App, props }) { + const root = createRoot(el); + + root.render(); + }, + progress: { + color: '#4B5563', + }, +}); diff --git a/stubs/inertia-react/resources/js/bootstrap.js b/stubs/inertia-react/resources/js/bootstrap.js new file mode 100644 index 0000000..846d350 --- /dev/null +++ b/stubs/inertia-react/resources/js/bootstrap.js @@ -0,0 +1,32 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo'; + +// import Pusher from 'pusher-js'; +// window.Pusher = Pusher; + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: import.meta.env.VITE_PUSHER_APP_KEY, +// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', +// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, +// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, +// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, +// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', +// enabledTransports: ['ws', 'wss'], +// }); diff --git a/stubs/inertia-react/resources/views/app.blade.php b/stubs/inertia-react/resources/views/app.blade.php new file mode 100644 index 0000000..856bcf2 --- /dev/null +++ b/stubs/inertia-react/resources/views/app.blade.php @@ -0,0 +1,22 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @routes + @viteReactRefresh + @vite(['resources/js/app.jsx', "resources/js/Pages/{$page['component']}.jsx"]) + @inertiaHead + + + @inertia + + diff --git a/stubs/inertia-react/resources/views/welcome.blade.php b/stubs/inertia-react/resources/views/welcome.blade.php new file mode 100644 index 0000000..0406510 --- /dev/null +++ b/stubs/inertia-react/resources/views/welcome.blade.php @@ -0,0 +1,140 @@ + + + + + + + Laravel + + + + + + + + + + + + diff --git a/stubs/inertia-react/routes/auth.php b/stubs/inertia-react/routes/auth.php new file mode 100644 index 0000000..1040b51 --- /dev/null +++ b/stubs/inertia-react/routes/auth.php @@ -0,0 +1,59 @@ +group(function () { + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); + + Route::post('register', [RegisteredUserController::class, 'store']); + + Route::get('login', [AuthenticatedSessionController::class, 'create']) + ->name('login'); + + Route::post('login', [AuthenticatedSessionController::class, 'store']); + + Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) + ->name('password.request'); + + Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) + ->name('password.email'); + + Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) + ->name('password.reset'); + + Route::post('reset-password', [NewPasswordController::class, 'store']) + ->name('password.store'); +}); + +Route::middleware('auth')->group(function () { + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); + + Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) + ->name('password.confirm'); + + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); +}); diff --git a/stubs/inertia-react/routes/web.php b/stubs/inertia-react/routes/web.php new file mode 100644 index 0000000..2dc822c --- /dev/null +++ b/stubs/inertia-react/routes/web.php @@ -0,0 +1,38 @@ + Route::has('login'), + 'canRegister' => Route::has('register'), + 'laravelVersion' => Application::VERSION, + 'phpVersion' => PHP_VERSION, + ]); +}); + +Route::get('/dashboard', function () { + return Inertia::render('Dashboard'); +})->middleware(['auth', 'verified'])->name('dashboard'); + +Route::middleware('auth')->group(function () { + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +}); + +require __DIR__ . '/auth.php'; diff --git a/stubs/inertia-react/tailwind.config.js b/stubs/inertia-react/tailwind.config.js new file mode 100644 index 0000000..5045de8 --- /dev/null +++ b/stubs/inertia-react/tailwind.config.js @@ -0,0 +1,20 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', + './storage/framework/views/*.php', + './resources/views/**/*.blade.php', + './resources/js/**/*.jsx', + ], + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + }, + + plugins: [require('@tailwindcss/forms')], +}; diff --git a/stubs/inertia-react/vite.config.js b/stubs/inertia-react/vite.config.js new file mode 100644 index 0000000..87fe8fb --- /dev/null +++ b/stubs/inertia-react/vite.config.js @@ -0,0 +1,13 @@ +import react from '@vitejs/plugin-react'; +import laravel from 'laravel-vite-plugin'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: 'resources/js/app.jsx', + refresh: true, + }), + react(), + ], +});