├── .prettierignore ├── .prettierrc.json ├── .vscode ├── settings.json └── tailwind.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── composer.json ├── config └── modular.php ├── eslint.config.js ├── package-lock.json ├── package.json ├── pint.json ├── rector.php ├── resources ├── lang │ ├── en │ │ ├── en.json │ │ └── validation.php │ └── pt_BR │ │ └── pt_BR.json └── views │ └── .gitkeep ├── src ├── Components │ └── Translations.php ├── Console │ ├── InstallCommand.php │ ├── InstallerTraits │ │ ├── BackendPackages.php │ │ ├── CoreModules.php │ │ ├── FrontendPackages.php │ │ ├── ModuleExists.php │ │ └── PestTests.php │ ├── MakeComponentCommand.php │ ├── MakeComposableCommand.php │ ├── MakeControllerCommand.php │ ├── MakeFactoryCommand.php │ ├── MakeMigrationCommand.php │ ├── MakeModelCommand.php │ ├── MakeModuleCommand.php │ ├── MakePageCommand.php │ ├── MakeRouteCommand.php │ ├── MakeSeederCommand.php │ ├── MakeServiceCommand.php │ ├── MakeTestCommand.php │ ├── MakeValidateCommand.php │ ├── PublishLaravelTranslationsCommand.php │ ├── PublishSiteFilesCommand.php │ ├── RegisterServiceProviderCommand.php │ └── SiteTraits │ │ ├── ConfigureViews.php │ │ ├── CopySiteFiles.php │ │ └── PublishConfigFile.php └── ModularServiceProvider.php └── stubs ├── .prettierrc.json ├── app └── Http │ └── Middleware │ └── HandleInertiaRequests.php ├── config └── auth.php ├── database └── seeders │ └── DatabaseSeeder.php ├── lang ├── bn │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php └── pt_BR │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── module-stub └── modules │ ├── Database │ ├── Factories │ │ └── ModelFactory.stub │ ├── Migrations │ │ └── create_table.stub │ └── Seeders │ │ └── ModuleSeeder.stub │ ├── Http │ ├── Controllers │ │ └── ModuleController.stub │ └── Requests │ │ └── ModuleValidate.stub │ ├── Models │ └── Model.stub │ ├── ModuleServiceProvider.stub │ ├── Services │ └── Service.stub │ ├── Tests │ └── ModuleTest.stub │ └── routes │ └── app.stub ├── modules ├── Acl │ ├── AclServiceProvider.php │ ├── Database │ │ └── Seeders │ │ │ ├── AclPermissionSeeder.php │ │ │ └── AclRoleSeeder.php │ ├── Http │ │ ├── Controllers │ │ │ ├── PermissionController.php │ │ │ ├── RoleController.php │ │ │ ├── RolePermissionController.php │ │ │ ├── UserController.php │ │ │ ├── UserPermissionController.php │ │ │ └── UserRoleController.php │ │ └── Requests │ │ │ ├── PermissionValidate.php │ │ │ └── RoleValidate.php │ ├── Services │ │ ├── GetUserPermissions.php │ │ └── ListUserPermissions.php │ ├── Tests │ │ ├── Permission │ │ │ ├── GetUserPermissionTest.php │ │ │ ├── PermissionTest.php │ │ │ └── UserPermissionTest.php │ │ └── Role │ │ │ ├── RolePermissionTest.php │ │ │ ├── RoleTest.php │ │ │ └── UserRoleTest.php │ ├── config │ │ └── config.php │ └── routes │ │ └── app.php ├── AdminAuth │ ├── AdminAuthServiceProvider.php │ ├── Http │ │ ├── Controllers │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── NewPasswordController.php │ │ │ └── PasswordResetLinkController.php │ │ ├── Middleware │ │ │ └── UserAuth.php │ │ └── Requests │ │ │ └── LoginRequest.php │ ├── Notifications │ │ └── ResetPassword.php │ ├── Tests │ │ ├── AuthenticationTest.php │ │ └── PasswordResetTest.php │ ├── routes │ │ └── site.php │ └── views │ │ ├── emails │ │ └── reset-password.blade.php │ │ ├── forgot-password-form.blade.php │ │ ├── layouts │ │ └── master.blade.php │ │ ├── login-form.blade.php │ │ └── reset-password-form.blade.php ├── Dashboard │ ├── DashboardServiceProvider.php │ ├── Http │ │ └── Controllers │ │ │ └── DashboardController.php │ ├── Tests │ │ └── DashboardTest.php │ └── routes │ │ └── app.php ├── Support │ ├── BaseServiceProvider.php │ ├── Http │ │ ├── Controllers │ │ │ ├── AppController.php │ │ │ └── BackendController.php │ │ └── Requests │ │ │ ├── JsonRequest.php │ │ │ └── Request.php │ ├── Models │ │ └── BaseModel.php │ ├── SupportServiceProvider.php │ ├── Tests │ │ ├── ActivityLogTraitTest.php │ │ ├── EditorImageTraitTest.php │ │ ├── FileNameGeneratorTraitTest.php │ │ ├── SearchableTraitTest.php │ │ ├── UpdateOrderTraitTest.php │ │ └── UploadFileTraitTest.php │ ├── Traits │ │ ├── ActivityLog.php │ │ ├── EditorImage.php │ │ ├── FileNameGenerator.php │ │ ├── Searchable.php │ │ ├── UpdateOrder.php │ │ └── UploadFile.php │ ├── Validators │ │ ├── recaptcha.php │ │ └── required_editor.php │ └── helpers.php └── User │ ├── Console │ └── Commands │ │ └── CreateUserCommand.php │ ├── Database │ ├── Factories │ │ └── UserFactory.php │ └── Migrations │ │ └── 2024_01_25_000000_add_custom_fields_to_users_table.php │ ├── Http │ ├── Controllers │ │ └── UserController.php │ └── Requests │ │ └── UserValidate.php │ ├── Models │ └── User.php │ ├── Observers │ └── UserObserver.php │ ├── Tests │ ├── CreateUserCommandTest.php │ └── UserTest.php │ ├── UserServiceProvider.php │ └── routes │ └── app.php ├── page-stub ├── Components │ └── Component.stub ├── Composables │ └── Composable.stub ├── Form.stub └── Index.stub ├── public └── favicon.svg ├── resources ├── css │ └── app.css ├── images │ └── logo.svg ├── js │ ├── Components │ │ ├── Auth │ │ │ ├── AppAuthLogo.vue │ │ │ └── AppAuthShell.vue │ │ ├── DataTable │ │ │ ├── AppDataSearch.vue │ │ │ ├── AppDataTable.vue │ │ │ ├── AppDataTableData.vue │ │ │ ├── AppDataTableHead.vue │ │ │ ├── AppDataTableRow.vue │ │ │ └── AppPaginator.vue │ │ ├── Form │ │ │ ├── AppCheckbox.vue │ │ │ ├── AppCombobox.vue │ │ │ ├── AppFormErrors.vue │ │ │ ├── AppInputDate.vue │ │ │ ├── AppInputFile.vue │ │ │ ├── AppInputPassword.vue │ │ │ ├── AppInputText.vue │ │ │ ├── AppLabel.vue │ │ │ ├── AppRadioButton.vue │ │ │ ├── AppTextArea.vue │ │ │ ├── AppTipTapEditor.vue │ │ │ └── TipTap │ │ │ │ ├── TipTapButton.vue │ │ │ │ ├── TipTapDivider.vue │ │ │ │ └── extension-file-upload.js │ │ ├── Menu │ │ │ ├── AppBreadCrumb.vue │ │ │ ├── AppBreadCrumbItem.vue │ │ │ ├── AppMenu.vue │ │ │ ├── AppMenuItem.vue │ │ │ └── AppMenuSection.vue │ │ ├── Message │ │ │ ├── AppAlert.vue │ │ │ ├── AppFlashMessage.vue │ │ │ ├── AppToast.vue │ │ │ └── AppTooltip.vue │ │ ├── Misc │ │ │ ├── AppButton.vue │ │ │ ├── AppCard.vue │ │ │ ├── AppImageNotAvailable.vue │ │ │ ├── AppLink.vue │ │ │ ├── AppSectionHeader.vue │ │ │ └── AppTopBar.vue │ │ └── Overlay │ │ │ ├── AppConfirmDialog.vue │ │ │ ├── AppModal.vue │ │ │ └── AppSideBar.vue │ ├── Composables │ │ ├── useAuthCan.js │ │ ├── useClickOutside.js │ │ ├── useDataSearch.js │ │ ├── useFormContext.js │ │ ├── useFormErrors.js │ │ ├── useIsMobile.js │ │ └── useTitle.js │ ├── Configs │ │ └── menu.js │ ├── Layouts │ │ ├── AuthenticatedLayout.vue │ │ └── GuestLayout.vue │ ├── Pages │ │ ├── AclPermission │ │ │ ├── PermissionForm.vue │ │ │ └── PermissionIndex.vue │ │ ├── AclRole │ │ │ ├── RoleForm.vue │ │ │ └── RoleIndex.vue │ │ ├── AclRolePermission │ │ │ └── RolePermissionForm.vue │ │ ├── AclUserPermission │ │ │ └── UserPermissionForm.vue │ │ ├── AclUserRole │ │ │ └── UserRoleForm.vue │ │ ├── AdminAuth │ │ │ ├── ForgotPage.vue │ │ │ ├── LoginForm.vue │ │ │ └── ResetPassword.vue │ │ ├── Dashboard │ │ │ ├── Components │ │ │ │ └── DashboardCard.vue │ │ │ └── DashboardIndex.vue │ │ └── User │ │ │ ├── UserForm.vue │ │ │ └── UserIndex.vue │ ├── Plugins │ │ └── Translations.js │ ├── Resolvers │ │ └── AppComponentsResolver.js │ ├── Utils │ │ ├── chunk.js │ │ ├── debounce.js │ │ ├── slug.js │ │ └── truncate.js │ └── app.js └── views │ ├── app.blade.php │ └── components │ └── translations.blade.php ├── routes └── web.php ├── site ├── modules │ ├── Index │ │ ├── Http │ │ │ ├── Controllers │ │ │ │ └── IndexController.php │ │ │ └── Requests │ │ │ │ └── IndexValidate.php │ │ ├── IndexServiceProvider.php │ │ ├── Models │ │ │ └── Index.php │ │ ├── Tests │ │ │ └── IndexTest.php │ │ ├── routes │ │ │ └── site.php │ │ └── views │ │ │ └── index.blade.php │ └── Support │ │ ├── SiteController.php │ │ └── SiteModel.php └── resources-site │ ├── css │ └── site.css │ ├── images │ └── home-img.png │ ├── js │ ├── Components │ │ └── IndexExampleComponent.vue │ ├── create-vue-app.js │ └── index-app.js │ └── views │ ├── pagination │ ├── simple-tailwind.blade.php │ └── tailwind.blade.php │ └── site-layout.blade.php ├── stack-configs ├── .prettierrc.json ├── .vscode │ ├── settings.json │ └── tailwind.json ├── eslint.config.js ├── jsconfig.json ├── postcss.config.mjs ├── tailwind.config.mjs └── vite.config.js └── tests ├── CreatesApplication.php ├── Pest.php └── TestCase.php /.prettierignore: -------------------------------------------------------------------------------- 1 | stubs/stack-configs/* 2 | stubs/site/stack-configs/* -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.customData": [".vscode/tailwind.json"], 3 | 4 | "[php]": { 5 | "editor.defaultFormatter": "open-southeners.laravel-pint", 6 | "editor.formatOnSave": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `modular` will be documented in this file. 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Daniel Coimbra Cintra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover a security vulnerability within Modular, please send an e-mail to Daniel Cintra via [modular@visualcom.com.br](mailto:modular@visualcom.com.br). All security vulnerabilities will be promptly addressed. 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daniel-cintra/modular", 3 | "description": "A fast way to develop web apps using Laravel, Vue and Inertia.", 4 | "keywords": [ 5 | "modular", 6 | "laravel", 7 | "modular" 8 | ], 9 | "homepage": "https://ismodular.com/", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Daniel Coimbra Cintra", 14 | "email": "danic10@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "illuminate/contracts": "^12.0", 21 | "inertiajs/inertia-laravel": "^2.0", 22 | "laravel/prompts": "^0.3.5", 23 | "spatie/laravel-activitylog": "^4.10", 24 | "spatie/laravel-package-tools": "^1.19", 25 | "spatie/laravel-permission": "^6.15", 26 | "tightenco/ziggy": "^2.5" 27 | }, 28 | "require-dev": { 29 | "laravel/pint": "^1.13", 30 | "nunomaduro/collision": "^8.6", 31 | "orchestra/testbench": "^10.0", 32 | "pestphp/pest": "^3.7", 33 | "pestphp/pest-plugin-laravel": "^3.1" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Modular\\Modular\\": "src", 38 | "Modular\\Modular\\Database\\Factories\\": "database/factories" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Modular\\Modular\\Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "analyse": "vendor/bin/phpstan analyse", 48 | "test": "vendor/bin/pest", 49 | "test-coverage": "vendor/bin/pest --coverage", 50 | "format": "vendor/bin/pint" 51 | }, 52 | "config": { 53 | "sort-packages": true, 54 | "allow-plugins": { 55 | "pestphp/pest-plugin": true, 56 | "phpstan/extension-installer": true 57 | } 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "Modular\\Modular\\ModularServiceProvider" 63 | ] 64 | } 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true 68 | } 69 | -------------------------------------------------------------------------------- /config/modular.php: -------------------------------------------------------------------------------- 1 | '/', 5 | 'default-logged-route' => 'dashboard.index', 6 | ]; 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import vue from 'eslint-plugin-vue' 3 | import vueParser from 'vue-eslint-parser' 4 | import prettierConfig from 'eslint-config-prettier' 5 | import globals from 'globals' 6 | 7 | export default [ 8 | // Base ESLint recommended rules 9 | js.configs.recommended, 10 | 11 | // Prettier configuration to disable conflicting rules 12 | { 13 | rules: { 14 | ...prettierConfig.rules, 15 | 'unicode-bom': 'off' 16 | } 17 | }, 18 | 19 | // Vue plugin configuration 20 | { 21 | files: ['**/*.vue'], 22 | languageOptions: { 23 | parser: vueParser, 24 | parserOptions: { 25 | ecmaVersion: 'latest', 26 | sourceType: 'module' 27 | } 28 | }, 29 | plugins: { 30 | vue 31 | }, 32 | rules: { 33 | // Combine base and recommended Vue rules 34 | ...vue.configs.base.rules, 35 | ...vue.configs['vue3-recommended'].rules, 36 | 37 | '@stylistic/js/indent': 'off', 38 | '@stylistic/js/quotes': 'off', 39 | 40 | // Disable specific Vue rules 41 | 'vue/no-v-html': 'off', 42 | 'vue/comment-directive': 'off' 43 | 44 | // You can add other Vue-specific rules here 45 | } 46 | }, 47 | 48 | // General JavaScript rules (for .js and .vue files) 49 | { 50 | files: ['**/*.{js,vue}'], 51 | rules: { 52 | // Disable general ESLint rules 53 | // 'no-undef': 'off' 54 | } 55 | }, 56 | 57 | // Custom rules (if any) 58 | { 59 | languageOptions: { 60 | globals: { 61 | ...globals.browser, 62 | route: 'readonly' 63 | } 64 | }, 65 | rules: { 66 | // Add your custom rules here 67 | } 68 | }, 69 | 70 | // Ignore patterns 71 | { 72 | ignores: ['node_modules/*', 'vendor/*'] 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "lint": "eslint \"**/*.{js,vue}\"", 6 | "format": "prettier --write ." 7 | }, 8 | "devDependencies": { 9 | "eslint": "^9.5.0", 10 | "eslint-config-prettier": "^10.0.2", 11 | "eslint-plugin-vue": "^9.32.0", 12 | "prettier": "^3.4.2", 13 | "vue-eslint-parser": "^9.4.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel" 3 | } 4 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | // __DIR__.'/config', 12 | // __DIR__.'/resources', 13 | // __DIR__.'/src', 14 | // __DIR__.'/tests', 15 | // __DIR__.'/stubs/app', 16 | // __DIR__.'/stubs/config', 17 | // __DIR__.'/stubs/database', 18 | // __DIR__.'/stubs/lang', 19 | // __DIR__.'/stubs/routes', 20 | ]); 21 | 22 | // register a single rule 23 | // $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 24 | 25 | // define sets of rules 26 | $rectorConfig->sets([ 27 | LevelSetList::UP_TO_PHP_82, 28 | ]); 29 | }; 30 | -------------------------------------------------------------------------------- /resources/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute field is required.', 5 | ]; 6 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular/62510efcd923a313442b9288b83ab0d1d22c778d/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Components/Translations.php: -------------------------------------------------------------------------------- 1 | getLocale(); 16 | 17 | if (app()->environment('production')) { 18 | $translations = cache()->rememberForever("translations_$locale", fn () => $this->getTranslations($locale)); 19 | } else { 20 | $translations = $this->getTranslations($locale); 21 | } 22 | 23 | return view('components.translations', [ 24 | 'translations' => $translations, 25 | ]); 26 | } 27 | 28 | private function getTranslations(string $locale): array 29 | { 30 | $appPHPTranslations = $this->getPHPTranslations(lang_path($locale)); 31 | 32 | $appJsonTranslations = $this->getJsonTranslations(lang_path("$locale.json")); 33 | $modularJsonTranslations = $this->getJsonTranslations(lang_path("vendor/modular/$locale/$locale.json")); 34 | 35 | return array_merge($appPHPTranslations, $appJsonTranslations, $modularJsonTranslations); 36 | } 37 | 38 | private function getPHPTranslations(string $directory): array 39 | { 40 | if (! is_dir($directory)) { 41 | return []; 42 | } 43 | 44 | return collect(File::allFiles($directory)) 45 | ->filter(fn ($file) => $file->getExtension() === 'php')->flatMap(fn ($file) => Arr::dot(File::getRequire($file->getRealPath())))->toArray(); 46 | } 47 | 48 | private function getJsonTranslations(string $filePath): array 49 | { 50 | if (File::exists($filePath)) { 51 | return json_decode(File::get($filePath), true, 512, JSON_THROW_ON_ERROR); 52 | } else { 53 | return []; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | components->info('Installing required stacks...'); 23 | 24 | $this->setupBackendPackages(); 25 | 26 | $this->installFrontendPackages(); 27 | 28 | $this->components->info('Required stacks installed!'); 29 | 30 | $this->configureCoreModules(); 31 | 32 | $this->setupPestTests(); 33 | 34 | $this->info('🎉 Modular successfully installed!'); 35 | 36 | $this->info('🚀 To create your first user, please run: php artisan modular:create-user'); 37 | 38 | return self::SUCCESS; 39 | } 40 | 41 | protected function phpBinary(): string 42 | { 43 | return (new PhpExecutableFinder)->find(false) ?: 'php'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Console/InstallerTraits/BackendPackages.php: -------------------------------------------------------------------------------- 1 | components->info('Publishing vendor files...'); 12 | $this->publishVendorFiles(); 13 | } 14 | 15 | protected function publishVendorFiles(): void 16 | { 17 | (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--provider=Spatie\Permission\PermissionServiceProvider'], base_path())) 18 | ->setTimeout(null) 19 | ->run(function ($type, $output) { 20 | $this->output->write($output); 21 | }); 22 | 23 | (new Process([$this->phpBinary(), 'artisan', 'config:clear'], base_path())) 24 | ->setTimeout(null) 25 | ->run(function ($type, $output) { 26 | $this->output->write($output); 27 | }); 28 | 29 | (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--provider=Spatie\Activitylog\ActivitylogServiceProvider', '--tag=activitylog-migrations'], base_path())) 30 | ->setTimeout(null) 31 | ->run(function ($type, $output) { 32 | $this->output->write($output); 33 | }); 34 | 35 | (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--provider=Spatie\Activitylog\ActivitylogServiceProvider', '--tag=activitylog-config'], base_path())) 36 | ->setTimeout(null) 37 | ->run(function ($type, $output) { 38 | $this->output->write($output); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Console/InstallerTraits/ModuleExists.php: -------------------------------------------------------------------------------- 1 | moduleName}"))) { 10 | $this->components->error("Module {$this->moduleName} does not exist."); 11 | 12 | return false; 13 | } 14 | 15 | return true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Console/MakeComponentCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 25 | $this->componentName = Str::studly($this->argument('componentName')); 26 | 27 | if (! $this->moduleExists()) { 28 | return self::FAILURE; 29 | } 30 | 31 | $this->comment('Module '.$this->moduleName.' found, creating Component...'); 32 | $this->createComponent(); 33 | 34 | $this->generateComments(); 35 | 36 | return self::SUCCESS; 37 | } 38 | 39 | private function createComponent(): void 40 | { 41 | $stub = file_get_contents(__DIR__.'/../../stubs/page-stub/Components/Component.stub'); 42 | 43 | $stub = str_replace('{{ componentName }}', Str::camel($this->componentName), $stub); 44 | 45 | (new Filesystem)->ensureDirectoryExists(resource_path("js/Pages/{$this->moduleName}/Components/")); 46 | 47 | $path = resource_path("js/Pages/{$this->moduleName}/Components/{$this->componentName}.vue"); 48 | 49 | file_put_contents($path, $stub); 50 | } 51 | 52 | private function generateComments(): void 53 | { 54 | $this->comment('In your Vue Component, import the new created Component as:'); 55 | $this->info("import {$this->componentName} from './Components/{$this->componentName}.vue'"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Console/MakeComposableCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 25 | $this->composableName = Str::studly($this->argument('composableName')); 26 | 27 | if (! $this->moduleExists()) { 28 | return self::FAILURE; 29 | } 30 | 31 | $this->comment('Module '.$this->moduleName.' found, creating Composable...'); 32 | $this->createComposable(); 33 | 34 | $this->generateComments(); 35 | 36 | return self::SUCCESS; 37 | } 38 | 39 | private function createComposable(): void 40 | { 41 | $stub = file_get_contents(__DIR__.'/../../stubs/page-stub/Composables/Composable.stub'); 42 | 43 | $stub = str_replace('{{ ComposableName }}', $this->composableName, $stub); 44 | $stub = str_replace('{{ composableName }}', Str::camel($this->composableName), $stub); 45 | 46 | (new Filesystem)->ensureDirectoryExists(resource_path("js/Pages/{$this->moduleName}/Composables/")); 47 | 48 | $path = resource_path("js/Pages/{$this->moduleName}/Composables/use{$this->composableName}.js"); 49 | 50 | file_put_contents($path, $stub); 51 | } 52 | 53 | private function generateComments(): void 54 | { 55 | $camelCaseComposableName = Str::camel($this->composableName); 56 | 57 | $this->comment('In your Vue Component, import the composable:'); 58 | $this->info("import use{$this->composableName} from './Composables/use{$this->composableName}'"); 59 | 60 | $this->comment('And use it like:'); 61 | $this->info("const { {$camelCaseComposableName} } = use{$this->composableName}()"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Console/MakeFactoryCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 25 | $this->resourceName = Str::studly($this->argument('resourceName')); 26 | 27 | if (! $this->moduleExists()) { 28 | return self::FAILURE; 29 | } 30 | 31 | $this->comment('Module '.$this->moduleName.' found, creating Factory...'); 32 | $this->createModuleFactory(); 33 | 34 | return self::SUCCESS; 35 | } 36 | 37 | private function createModuleFactory(): void 38 | { 39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Database/Factories")); 40 | 41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Database/Factories/ModelFactory.stub'); 42 | 43 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub); 44 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub); 45 | $stub = str_replace('{{ resourceName }}', Str::camel($this->resourceName), $stub); 46 | 47 | $path = base_path("modules/{$this->moduleName}/Database/Factories/{$this->resourceName}Factory.php"); 48 | 49 | file_put_contents($path, $stub); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Console/MakeMigrationCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 25 | $this->migrationName = $this->argument('migrationName'); 26 | 27 | if (! $this->moduleExists()) { 28 | return self::FAILURE; 29 | } 30 | 31 | $this->comment('Module '.$this->moduleName.' found, creating migration file...'); 32 | $this->createMigrationFile(); 33 | 34 | return self::SUCCESS; 35 | } 36 | 37 | private function createMigrationFile(): void 38 | { 39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Database/Migrations")); 40 | 41 | $this->call('make:migration', [ 42 | 'name' => $this->migrationName, 43 | '--path' => "modules/{$this->moduleName}/Database/Migrations", 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Console/MakeModelCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 24 | $this->resourceName = Str::studly($this->argument('resourceName')); 25 | 26 | if (! $this->moduleExists()) { 27 | return self::FAILURE; 28 | } 29 | 30 | $this->comment('Module '.$this->moduleName.' found, creating Model...'); 31 | $this->createModuleModel(); 32 | 33 | return self::SUCCESS; 34 | } 35 | 36 | private function createModuleModel(): void 37 | { 38 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Models/Model.stub'); 39 | 40 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub); 41 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub); 42 | $stub = str_replace('{{ resourceName }}', Str::camel(Str::plural($this->resourceName)), $stub); 43 | 44 | $path = base_path("modules/{$this->moduleName}/Models/{$this->resourceName}.php"); 45 | 46 | file_put_contents($path, $stub); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Console/MakeRouteCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 24 | $this->resourceName = Str::studly($this->argument('resourceName')); 25 | 26 | if (! $this->moduleExists()) { 27 | return self::FAILURE; 28 | } 29 | 30 | $this->comment('Module '.$this->moduleName.' found, creating Route...'); 31 | $this->createModuleRoute(); 32 | 33 | return self::SUCCESS; 34 | } 35 | 36 | private function createModuleRoute(): void 37 | { 38 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/routes/app.stub'); 39 | 40 | $stub = str_replace('{{ resource-name }}', Str::snake($this->resourceName, '-'), $stub); 41 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub); 42 | $stub = str_replace('{{ resourceName }}', Str::camel($this->resourceName), $stub); 43 | 44 | $path = base_path("modules/{$this->moduleName}/routes/app.php"); 45 | 46 | if (! file_exists($path)) { 47 | file_put_contents($path, $stub); 48 | } else { 49 | $stub = str_replace('resourceName).' routes', $stub); 51 | 52 | file_put_contents($path, $stub, FILE_APPEND); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Console/MakeSeederCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 25 | $this->resourceName = Str::studly($this->argument('resourceName')); 26 | 27 | if (! $this->moduleExists()) { 28 | return self::FAILURE; 29 | } 30 | 31 | $this->comment('Module '.$this->moduleName.' found, creating seeder file...'); 32 | $this->createModuleSeeder(); 33 | 34 | return self::SUCCESS; 35 | } 36 | 37 | private function createModuleSeeder(): void 38 | { 39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Database/Seeders")); 40 | 41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Database/Seeders/ModuleSeeder.stub'); 42 | 43 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub); 44 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub); 45 | 46 | $path = base_path("modules/{$this->moduleName}/Database/Seeders/{$this->resourceName}.php"); 47 | 48 | file_put_contents($path, $stub); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/MakeServiceCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 25 | $this->serviceName = Str::studly($this->argument('serviceName')); 26 | 27 | if (! $this->moduleExists()) { 28 | return self::FAILURE; 29 | } 30 | 31 | $this->comment('Module '.$this->moduleName.' found, creating Service...'); 32 | $this->createModuleService(); 33 | 34 | return self::SUCCESS; 35 | } 36 | 37 | private function createModuleService(): void 38 | { 39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Services/")); 40 | 41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Services/Service.stub'); 42 | 43 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub); 44 | $stub = str_replace('{{ ServiceName }}', $this->serviceName, $stub); 45 | 46 | $path = base_path("modules/{$this->moduleName}/Services/{$this->serviceName}.php"); 47 | 48 | file_put_contents($path, $stub); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/MakeTestCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 25 | $this->testFileName = $this->argument('resourceName'); 26 | 27 | if (! $this->moduleExists()) { 28 | return self::FAILURE; 29 | } 30 | 31 | $this->comment('Module '.$this->moduleName.' found, creating test file...'); 32 | $this->createTestFile(); 33 | 34 | return self::SUCCESS; 35 | } 36 | 37 | private function createTestFile(): void 38 | { 39 | (new Filesystem)->ensureDirectoryExists(base_path("modules/{$this->moduleName}/Tests/")); 40 | 41 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Tests/ModuleTest.stub'); 42 | 43 | $path = base_path("modules/{$this->moduleName}/Tests/{$this->testFileName}Test.php"); 44 | 45 | file_put_contents($path, $stub); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Console/MakeValidateCommand.php: -------------------------------------------------------------------------------- 1 | moduleName = Str::studly($this->argument('moduleName')); 24 | $this->resourceName = Str::studly($this->argument('resourceName')); 25 | 26 | if (! $this->moduleExists()) { 27 | return self::FAILURE; 28 | } 29 | 30 | $this->comment('Module '.$this->moduleName.' found, creating HTTP Request Validate...'); 31 | $this->createValidateFile(); 32 | 33 | return self::SUCCESS; 34 | } 35 | 36 | private function createValidateFile(): void 37 | { 38 | $stub = file_get_contents(__DIR__.'/../../stubs/module-stub/modules/Http/Requests/ModuleValidate.stub'); 39 | 40 | $stub = str_replace('{{ ModuleName }}', $this->moduleName, $stub); 41 | $stub = str_replace('{{ ResourceName }}', $this->resourceName, $stub); 42 | 43 | $path = base_path("modules/{$this->moduleName}/Http/Requests/{$this->resourceName}Validate.php"); 44 | 45 | file_put_contents($path, $stub); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Console/PublishLaravelTranslationsCommand.php: -------------------------------------------------------------------------------- 1 | option('lang'); 19 | 20 | $availableLangs = ['pt_BR']; 21 | 22 | if (! in_array($lang, $availableLangs)) { 23 | $this->error("The language '{$lang}' is not available. Available languages: ".implode(', ', $availableLangs)); 24 | 25 | return self::FAILURE; 26 | } 27 | 28 | $this->publishLang($lang); 29 | 30 | return self::SUCCESS; 31 | } 32 | 33 | private function publishLang(string $lang): void 34 | { 35 | $filesystem = new Filesystem; 36 | 37 | $filesystem->copyDirectory( 38 | __DIR__."/../../stubs/lang/{$lang}", 39 | lang_path("{$lang}") 40 | ); 41 | 42 | $this->info("The language files for '{$lang}' were published successfully."); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Console/PublishSiteFilesCommand.php: -------------------------------------------------------------------------------- 1 | publishModularConfigFile(); 21 | 22 | $this->copySupportModuleFiles(); 23 | $this->copyIndexModuleDir(); 24 | 25 | $this->copyResourcesSiteDir(); 26 | 27 | $this->configureViews(); 28 | 29 | return self::SUCCESS; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Console/RegisterServiceProviderCommand.php: -------------------------------------------------------------------------------- 1 | argument('name'); 16 | 17 | if ($this->registerServiceProvider($moduleName)) { 18 | $this->info("Service provider for module {$moduleName} registered successfully."); 19 | 20 | return self::SUCCESS; 21 | } 22 | 23 | $this->error("Service provider for module {$moduleName} is already registered."); 24 | 25 | return self::FAILURE; 26 | } 27 | 28 | public function registerServiceProvider(string $moduleName): bool 29 | { 30 | $providerPath = base_path('bootstrap/providers.php'); 31 | $content = file_get_contents($providerPath); 32 | 33 | $providerClass = "Modules\\{$moduleName}\\{$moduleName}ServiceProvider::class"; 34 | 35 | // Check if provider already exists 36 | if (strpos($content, $providerClass) !== false) { 37 | return false; 38 | } 39 | 40 | // Split content into lines 41 | $lines = explode("\n", $content); 42 | 43 | // Find the position of the closing bracket 44 | $returnArrayIndex = -1; 45 | foreach ($lines as $index => $line) { 46 | if (trim($line) === '];') { 47 | $returnArrayIndex = $index; 48 | break; 49 | } 50 | } 51 | 52 | if ($returnArrayIndex === -1) { 53 | return false; 54 | } 55 | 56 | // Insert the new provider before the closing bracket 57 | array_splice($lines, $returnArrayIndex, 0, " {$providerClass},"); 58 | 59 | // Join the lines back together 60 | $updatedContent = implode("\n", $lines); 61 | 62 | return (bool) file_put_contents($providerPath, $updatedContent); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Console/SiteTraits/ConfigureViews.php: -------------------------------------------------------------------------------- 1 | call('config:publish', [ 13 | 'name' => 'view', 14 | ]); 15 | } 16 | 17 | $this->info('Setting config/view.php...'); 18 | 19 | $viewConfig = file_get_contents($configViewFilePath); 20 | 21 | $strSearch = "resource_path('views'),"; 22 | $strReplace = "resource_path('views'),".PHP_EOL." base_path('resources-site/views'),"; 23 | 24 | $updatedViewConfig = str_replace( 25 | $strSearch, 26 | $strReplace, 27 | $viewConfig 28 | ); 29 | 30 | file_put_contents($configViewFilePath, $updatedViewConfig); 31 | 32 | $this->info('The config/view.php file was updated.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Console/SiteTraits/CopySiteFiles.php: -------------------------------------------------------------------------------- 1 | info('Copying Support Module files...'); 12 | 13 | copy(__DIR__.'/../../../stubs/site/modules/Support/SiteController.php', base_path('modules/Support/Http/Controllers/SiteController.php')); 14 | copy(__DIR__.'/../../../stubs/site/modules/Support/SiteModel.php', base_path('modules/Support/Models/SiteModel.php')); 15 | 16 | $this->info('Support Module files copied.'); 17 | } 18 | 19 | private function copyResourcesSiteDir(): void 20 | { 21 | $this->info('Copying resources-site directory...'); 22 | 23 | (new Filesystem)->ensureDirectoryExists(base_path('resources-site')); 24 | (new Filesystem)->copyDirectory(__DIR__.'/../../../stubs/site/resources-site', base_path('resources-site')); 25 | 26 | $this->info('The resources-site directory was copied.'); 27 | } 28 | 29 | private function copyIndexModuleDir(): void 30 | { 31 | $this->info('Copying Index Module directory...'); 32 | 33 | (new Filesystem)->ensureDirectoryExists(base_path('modules/Index')); 34 | (new Filesystem)->copyDirectory(__DIR__.'/../../../stubs/site/modules/Index', base_path('modules/Index')); 35 | 36 | $this->info('The Index Module directory was copied.'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/SiteTraits/PublishConfigFile.php: -------------------------------------------------------------------------------- 1 | info('The Modular config file already exists. Moving on...'); 11 | } else { 12 | $this->call('vendor:publish', [ 13 | '--tag' => 'modular-config', 14 | ]); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ModularServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('modular') 37 | ->hasConfigFile() 38 | ->hasTranslations() 39 | ->hasViewComponents('modular', Translations::class) 40 | ->hasCommand(InstallCommand::class) 41 | ->hasCommand(MakeModuleCommand::class) 42 | ->hasCommand(MakeControllerCommand::class) 43 | ->hasCommand(MakeValidateCommand::class) 44 | ->hasCommand(MakeModelCommand::class) 45 | ->hasCommand(MakeRouteCommand::class) 46 | ->hasCommand(MakeServiceCommand::class) 47 | ->hasCommand(MakePageCommand::class) 48 | ->hasCommand(MakeComposableCommand::class) 49 | ->hasCommand(MakeComponentCommand::class) 50 | ->hasCommand(MakeTestCommand::class) 51 | ->hasCommand(MakeMigrationCommand::class) 52 | ->hasCommand(MakeSeederCommand::class) 53 | ->hasCommand(PublishLaravelTranslationsCommand::class) 54 | ->hasCommand(PublishSiteFilesCommand::class) 55 | ->hasCommand(MakeFactoryCommand::class) 56 | ->hasCommand(RegisterServiceProviderCommand::class); 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /stubs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /stubs/app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | user(); 37 | 38 | return array_merge(parent::share($request), [ 39 | 'auth' => [ 40 | 'user' => $user, 41 | 'permissions' => $user ? (new ListUserPermissions)->run($user->id) : [], 42 | 'isRootUser' => $user ? ($user->hasRole('root') ? true : false) : false, 43 | ], 44 | 'ziggy' => fn () => array_merge((new Ziggy)->toArray(), [ 45 | 'location' => $request->url(), 46 | ]), 47 | 'flash' => fn () => [ 48 | 'success' => $request->session()->get('success'), 49 | 'error' => $request->session()->get('error'), 50 | ], 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /stubs/database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 14 | AclRoleSeeder::class, 15 | AclPermissionSeeder::class, 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stubs/lang/bn/auth.php: -------------------------------------------------------------------------------- 1 | 'এই পরিচয়পত্র আমাদের রেকর্ডের সাথে মেলে না।', 17 | 'password' => 'পাসওয়ার্ড ভুল।', 18 | 'throttle' => 'লগইন করার জন্য অনেকবার চেষ্টা করেছেন, :seconds সেকেন্ড পরে পুনরায় চেষ্টা করুন।', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /stubs/lang/bn/pagination.php: -------------------------------------------------------------------------------- 1 | '« পূর্বে', 17 | 'next' => 'পরবর্তি »', 18 | 19 | 'Showing' => 'দেখাচ্ছে', 20 | 'to' => 'পর্যন্ত', 21 | 'of' => 'এর মধ্যে', 22 | 'results' => 'ফলাফলসমুহ', 23 | 24 | ]; 25 | -------------------------------------------------------------------------------- /stubs/lang/bn/passwords.php: -------------------------------------------------------------------------------- 1 | 'আপনার পাসওয়ার্ড পুনরায় সেট করা হয়েছে!', 17 | 'sent' => 'আমরা আপনার পাসওয়ার্ড পুনরায় সেট করার লিঙ্ক ই-মেইল করেছি!', 18 | 'throttle' => 'লগইন করার জন্য অনেকবার চেষ্টা করেছেন, :seconds সেকেন্ড পরে পুনরায় চেষ্টা করুন।', 19 | 'token' => 'এই পাসওয়ার্ড রিসেট টোকেনটি সঠিক নয়।', 20 | 'user' => 'এই ই-মেইল দিয়ে কোন ব্যবহারকারী খুঁজে পাওয়া যাচ্ছে না', 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /stubs/lang/pt_BR/auth.php: -------------------------------------------------------------------------------- 1 | 'Credenciais incorretas.', 17 | 'password' => 'A senha informada está incorreta.', 18 | 'throttle' => 'Tentativas de login excedidas. Por favor tente novamente em :seconds segundos.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /stubs/lang/pt_BR/pagination.php: -------------------------------------------------------------------------------- 1 | '« Anterior', 17 | 'next' => 'Próxima »', 18 | 19 | 'Showing' => 'Mostrando', 20 | 'to' => 'até', 21 | 'of' => 'de', 22 | 'results' => 'resultados', 23 | 24 | ]; 25 | -------------------------------------------------------------------------------- /stubs/lang/pt_BR/passwords.php: -------------------------------------------------------------------------------- 1 | 'Sua senha foi redefinida!', 17 | 'sent' => 'Enviamos um email para redefinição da sua senha!', 18 | 'throttled' => 'Por favor aguarde para tentar novamente.', 19 | 'token' => 'O token para redefinição da senha é inválido.', 20 | 'user' => 'Nós não encontramos um usuário com este email definido.', 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /stubs/module-stub/modules/Database/Factories/ModelFactory.stub: -------------------------------------------------------------------------------- 1 | faker->unique()->sentence(4); 15 | 16 | return [ 17 | 'name' => $name, 18 | 19 | 'created_at' => $this->faker->dateTimeBetween('-1 year', '-6 month'), 20 | 'updated_at' => $this->faker->dateTimeBetween('-5 month', 'now'), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /stubs/module-stub/modules/Database/Migrations/create_table.stub: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->timestamps(); 18 | $table->softDeletes(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('${{ resourceName }}'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /stubs/module-stub/modules/Database/Seeders/ModuleSeeder.stub: -------------------------------------------------------------------------------- 1 | search(request('searchContext'), request('searchTerm')) 17 | ->paginate(request('rowsPerPage', 10)) 18 | ->withQueryString() 19 | ->through(fn (${{ resourceName }}) => [ 20 | 'id' => ${{ resourceName }}->id, 21 | 'name' => ${{ resourceName }}->name, 22 | 'created_at' => ${{ resourceName }}->created_at->format('d/m/Y H:i') . 'h' 23 | ]); 24 | 25 | return inertia('{{ ModuleName }}/{{ ResourceName }}Index', [ 26 | '{{ resourceNameCamelPlural }}' => ${{ resourceNameCamelPlural }} 27 | ]); 28 | } 29 | 30 | public function create(): Response 31 | { 32 | return inertia('{{ ModuleName }}/{{ ResourceName }}Form'); 33 | } 34 | 35 | public function store({{ ResourceName }}Validate $request): RedirectResponse 36 | { 37 | {{ ResourceName }}::create($request->validated()); 38 | 39 | return redirect()->route('{{ resourceName }}.index') 40 | ->with('success', '{{ ResourceName }} created.'); 41 | } 42 | 43 | public function edit(int $id): Response 44 | { 45 | ${{ resourceName }} = {{ ResourceName }}::find($id); 46 | 47 | return inertia('{{ ModuleName }}/{{ ResourceName }}Form', [ 48 | '{{ resourceName }}' => ${{ resourceName }} 49 | ]); 50 | } 51 | 52 | public function update({{ ResourceName }}Validate $request, int $id): RedirectResponse 53 | { 54 | ${{ resourceName }} = {{ ResourceName }}::findOrFail($id); 55 | 56 | ${{ resourceName }}->update($request->validated()); 57 | 58 | return redirect()->route('{{ resourceName }}.index') 59 | ->with('success', '{{ ResourceName }} updated.'); 60 | } 61 | 62 | public function destroy(int $id): RedirectResponse 63 | { 64 | {{ ResourceName }}::findOrFail($id)->delete(); 65 | 66 | return redirect()->route('{{ resourceName }}.index') 67 | ->with('success', '{{ ResourceName }} deleted.'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /stubs/module-stub/modules/Http/Requests/ModuleValidate.stub: -------------------------------------------------------------------------------- 1 | 'required', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stubs/module-stub/modules/Models/Model.stub: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/Database/Migrations'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stubs/module-stub/modules/Services/Service.stub: -------------------------------------------------------------------------------- 1 | assertTrue(true); 10 | // }); 11 | -------------------------------------------------------------------------------- /stubs/module-stub/modules/routes/app.stub: -------------------------------------------------------------------------------- 1 | name('{{ resourceName }}.index'); 9 | 10 | Route::get('{{ resource-name }}/create', [ 11 | {{ ResourceName }}Controller::class, 'create', 12 | ])->name('{{ resourceName }}.create'); 13 | 14 | Route::post('{{ resource-name }}', [ 15 | {{ ResourceName }}Controller::class, 'store', 16 | ])->name('{{ resourceName }}.store'); 17 | 18 | Route::get('{{ resource-name }}/{id}/edit', [ 19 | {{ ResourceName }}Controller::class, 'edit', 20 | ])->name('{{ resourceName }}.edit'); 21 | 22 | Route::put('{{ resource-name }}/{id}', [ 23 | {{ ResourceName }}Controller::class, 'update', 24 | ])->name('{{ resourceName }}.update'); 25 | 26 | Route::delete('{{ resource-name }}/{id}', [ 27 | {{ ResourceName }}Controller::class, 'destroy', 28 | ])->name('{{ resourceName }}.destroy'); 29 | -------------------------------------------------------------------------------- /stubs/modules/Acl/AclServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/config/config.php', 'acl'); 25 | 26 | Gate::before(function ($user, $ability) { 27 | return $user->hasRole('root') ? true : null; // Must be null, not false. (From Spatie Permission Package documentation.) 28 | }); 29 | 30 | parent::boot(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Database/Seeders/AclPermissionSeeder.php: -------------------------------------------------------------------------------- 1 | getPermissions(); 18 | 19 | foreach ($permissions as $permission) { 20 | Permission::create([ 21 | 'name' => $permission, 22 | 'guard_name' => 'user', 23 | ]); 24 | } 25 | 26 | Schema::enableForeignKeyConstraints(); 27 | } 28 | 29 | private function getPermissions(): array 30 | { 31 | return [ 32 | // Main Menu 33 | 'Dashboard', 34 | 35 | // Acl: Access Control List 36 | 'Acl', 37 | 'Acl: User - List', 38 | 'Acl: Permission - List', 39 | 'Acl: Role - List', 40 | 41 | // User/UserIndex.vue 42 | 'Acl: User: Role - Edit', 43 | 'Acl: User: Permission - Edit', 44 | 'Acl: User - Create', 45 | 'Acl: User - Edit', 46 | 'Acl: User - Delete', 47 | 48 | // AclPermission/PermissionIndex.vue 49 | 'Acl: Permission - Create', 50 | 'Acl: Permission - Edit', 51 | 'Acl: Permission - Delete', 52 | 53 | // AclRole/RoleIndex.vue 54 | 'Acl: Role - Create', 55 | 'Acl: Role - Edit', 56 | 'Acl: Role - Delete', 57 | 58 | // AclRolePermission/RolePermissionForm.vue 59 | 'Acl: Role: Permission - Edit', 60 | ]; 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Database/Seeders/AclRoleSeeder.php: -------------------------------------------------------------------------------- 1 | 'root', 19 | 'guard_name' => 'user', 20 | ]); 21 | 22 | Schema::enableForeignKeyConstraints(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Controllers/PermissionController.php: -------------------------------------------------------------------------------- 1 | where(request('searchContext'), 'like', "%{$searchTerm}%"); 16 | }) 17 | ->orderBy('guard_name') 18 | ->orderBy('name') 19 | ->paginate(request('rowsPerPage', 10)) 20 | ->withQueryString() 21 | ->through(fn ($permission) => [ 22 | 'id' => $permission->id, 23 | 'name' => $permission->name, 24 | 'guard' => $permission->guard, 25 | 26 | ]); 27 | 28 | return inertia('AclPermission/PermissionIndex', [ 29 | 'permissions' => $permissions, 30 | ]); 31 | } 32 | 33 | public function create() 34 | { 35 | return inertia('AclPermission/PermissionForm'); 36 | } 37 | 38 | public function store(PermissionValidate $request) 39 | { 40 | $params = $request->validated(); 41 | $params['guard_name'] = 'user'; 42 | Permission::create($params); 43 | 44 | return redirect()->route('aclPermission.index') 45 | ->with('success', 'Permission created'); 46 | } 47 | 48 | public function edit($id) 49 | { 50 | $permission = Permission::find($id); 51 | 52 | return inertia('AclPermission/PermissionForm', [ 53 | 'permission' => $permission, 54 | ]); 55 | } 56 | 57 | public function update(PermissionValidate $request, $id) 58 | { 59 | $permission = Permission::findOrFail($id); 60 | 61 | $permission->update($request->validated()); 62 | 63 | return redirect()->route('aclPermission.index') 64 | ->with('success', 'Permission updated'); 65 | } 66 | 67 | public function destroy($id) 68 | { 69 | Permission::findOrFail($id)->delete(); 70 | 71 | return redirect()->route('aclPermission.index') 72 | ->with('success', 'Permission deleted'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Controllers/RoleController.php: -------------------------------------------------------------------------------- 1 | where(request('searchContext'), 'like', "%{$searchTerm}%"); 16 | }) 17 | ->orderBy('name') 18 | ->paginate(request('rowsPerPage', 10)) 19 | ->withQueryString() 20 | ->through(fn ($role) => [ 21 | 'id' => $role->id, 22 | 'name' => $role->name, 23 | 'guard_name' => $role->guard_name, 24 | ]); 25 | 26 | return inertia('AclRole/RoleIndex', [ 27 | 'roles' => $roles, 28 | ]); 29 | } 30 | 31 | public function create() 32 | { 33 | return inertia('AclRole/RoleForm'); 34 | } 35 | 36 | public function store(RoleValidate $request) 37 | { 38 | $params = $request->validated(); 39 | $params['guard_name'] = 'user'; 40 | Role::create($params); 41 | 42 | return redirect()->route('aclRole.index') 43 | ->with('success', 'Role created'); 44 | } 45 | 46 | public function edit($id) 47 | { 48 | $role = Role::find($id); 49 | 50 | return inertia('AclRole/RoleForm', [ 51 | 'role' => $role, 52 | ]); 53 | } 54 | 55 | public function update(RoleValidate $request, $id) 56 | { 57 | $role = Role::findOrFail($id); 58 | 59 | $role->update($request->validated()); 60 | 61 | return redirect()->route('aclRole.index') 62 | ->with('success', 'Role updated'); 63 | } 64 | 65 | public function destroy($id) 66 | { 67 | Role::findOrFail($id)->delete(); 68 | 69 | return redirect()->route('aclRole.index') 70 | ->with('success', 'Role deleted'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Controllers/RolePermissionController.php: -------------------------------------------------------------------------------- 1 | function ($q) { 14 | $q->get(['id', 'name']); 15 | }])->findOrFail($id); 16 | 17 | $role->permissions->map(function ($permission) { 18 | unset($permission->pivot); 19 | 20 | return $permission; 21 | }); 22 | 23 | $permissions = Permission::orderBy('name')->get(['id', 'name']); 24 | 25 | return inertia('AclRolePermission/RolePermissionForm', [ 26 | 'role' => $role, 27 | 'permissions' => $permissions, 28 | ]); 29 | } 30 | 31 | public function update($id) 32 | { 33 | $role = Role::findOrFail($id); 34 | 35 | $role->syncPermissions(request('rolePermissions')); 36 | 37 | return redirect()->route('aclRole.index') 38 | ->with('success', 'Role permissions updated'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | orderBy('name')->get(); 14 | 15 | return compact('users'); 16 | } 17 | 18 | public function getUserRolesAndPermissions(Request $request) 19 | { 20 | $user = auth()->guard('user')->user(); 21 | 22 | return [ 23 | 'userRoles' => $user->getRoleNames(), 24 | 'userPermissions' => $user->getPermissionsViaRoles()->pluck('name'), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Controllers/UserPermissionController.php: -------------------------------------------------------------------------------- 1 | run($id); 16 | 17 | $permissions = Permission::orderBy('name')->get(['id', 'name']); 18 | 19 | return inertia('AclUserPermission/UserPermissionForm', [ 20 | 'user' => $user, 21 | 'userPermissions' => $userPermissions, 22 | 'permissions' => $permissions, 23 | ]); 24 | } 25 | 26 | public function update($id) 27 | { 28 | $user = User::findOrFail($id); 29 | 30 | $user->syncPermissions(request('userPermissions')); 31 | 32 | return redirect()->route('user.index') 33 | ->with('success', 'User permissions updated'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Controllers/UserRoleController.php: -------------------------------------------------------------------------------- 1 | function ($q) { 14 | $q->get(['id', 'name']); 15 | }])->findOrFail($id); 16 | 17 | $user->roles->map(function ($role) { 18 | unset($role->pivot); 19 | 20 | return $role; 21 | }); 22 | 23 | $roles = Role::orderBy('name')->get(['id', 'name']); 24 | 25 | return inertia('AclUserRole/UserRoleForm', [ 26 | 'user' => $user, 27 | 'roles' => $roles, 28 | ]); 29 | } 30 | 31 | public function update($id) 32 | { 33 | $user = User::findOrFail($id); 34 | 35 | $user->syncRoles(request('userRoles')); 36 | 37 | return redirect()->route('user.index') 38 | ->with('success', 'User roles updated'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Requests/PermissionValidate.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'required', 22 | 'string', 23 | 'min:3', 24 | 'max:255', 25 | Rule::unique(Permission::class)->ignore($this->id), 26 | ], 27 | 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Http/Requests/RoleValidate.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'required', 22 | 'string', 23 | 'min:2', 24 | 'max:255', 25 | Rule::unique(Role::class)->ignore($this->id), 26 | ], 27 | 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Services/GetUserPermissions.php: -------------------------------------------------------------------------------- 1 | function ($query) { 13 | $query->get(['id', 'name']); 14 | }])->findOrFail($userId); 15 | 16 | // if has direct permissions use it 17 | if ($user->permissions->count()) { 18 | return $this->mapPermissions($user->permissions); 19 | } 20 | 21 | // get the permissions via roles 22 | return $this->mapPermissions($user->getAllPermissions()); 23 | } 24 | 25 | private function mapPermissions(Collection $permissions): array 26 | { 27 | return $permissions->map(fn ($permission) => [ 28 | 'id' => $permission->id, 29 | 'name' => $permission->name, 30 | ])->toArray(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Services/ListUserPermissions.php: -------------------------------------------------------------------------------- 1 | run($userId); 12 | 13 | return Arr::pluck($userPermissions, 'name'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Tests/Permission/GetUserPermissionTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(); 14 | 15 | $this->role = Role::create(['name' => 'role 1', 'guard_name' => 'user']); 16 | $this->role2 = Role::create(['name' => 'role 2', 'guard_name' => 'user']); 17 | 18 | $this->permission = Permission::create(['name' => 'permission 1', 'guard_name' => 'user']); 19 | $this->permission2 = Permission::create(['name' => 'permission 2', 'guard_name' => 'user']); 20 | }); 21 | 22 | test('user permission service returns correct direct permissions for the user', function () { 23 | $this->user->syncPermissions([$this->permission->id]); 24 | 25 | $userPermissions = (new GetUserPermissions)->run($this->user->id); 26 | 27 | $this->assertCount(1, $userPermissions); 28 | $this->assertEquals($this->permission->id, $userPermissions[0]['id']); 29 | }); 30 | 31 | test('user permission service returns correct empty permissions for the user', function () { 32 | $userPermissions = (new GetUserPermissions)->run($this->user->id); 33 | 34 | $this->assertTrue(is_array($userPermissions)); 35 | $this->assertCount(0, $userPermissions); 36 | }); 37 | 38 | test('user permission service returns correct role permissions for the user', function () { 39 | $this->role2->syncPermissions([$this->permission2->id]); 40 | $this->user->syncRoles([$this->role2->id]); 41 | 42 | $userPermissions = (new GetUserPermissions)->run($this->user->id); 43 | 44 | $this->assertCount(1, $userPermissions); 45 | $this->assertEquals($this->permission2->id, $userPermissions[0]['id']); 46 | }); 47 | 48 | test('user permission service returns correct direct and role permissions for the user', function () { 49 | $this->user->syncPermissions([$this->permission->id]); 50 | $this->role2->syncPermissions([$this->permission2->id]); 51 | $this->user->syncRoles([$this->role2->id]); 52 | 53 | $userPermissions = (new GetUserPermissions)->run($this->user->id); 54 | 55 | // granular user permissions, will always override role permissions for the user 56 | $this->assertCount(1, $userPermissions); 57 | $this->assertEquals($this->permission->id, $userPermissions[0]['id']); 58 | }); 59 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Tests/Permission/UserPermissionTest.php: -------------------------------------------------------------------------------- 1 | 'root']); 15 | $this->user = User::factory()->create(); 16 | $this->user->assignRole($role); 17 | 18 | $this->loggedRequest = $this->actingAs($this->user); 19 | 20 | $this->permission = Permission::create(['name' => 'first', 'guard_name' => 'user']); 21 | $this->permission2 = Permission::create(['name' => 'second', 'guard_name' => 'user']); 22 | 23 | $this->user->syncPermissions([$this->permission->id]); 24 | }); 25 | 26 | test('user permissions can be rendered', function () { 27 | $response = $this->loggedRequest->get('/admin/acl-user-permission/'.$this->user->id.'/edit'); 28 | 29 | $response->assertStatus(200); 30 | 31 | $response->assertInertia( 32 | fn (Assert $page) => $page 33 | ->component('AclUserPermission/UserPermissionForm') 34 | ->has( 35 | 'user', 36 | fn (Assert $page) => $page 37 | ->where('id', $this->user->id) 38 | ->etc() 39 | ) 40 | ->has( 41 | 'userPermissions', 42 | 1, 43 | fn (Assert $page) => $page 44 | ->where('id', $this->permission->id) 45 | ->where('name', $this->permission->name) 46 | ) 47 | ->has( 48 | 'permissions', 49 | 2 50 | ) 51 | ); 52 | }); 53 | 54 | test('user permissions can be updated', function () { 55 | $response = $this->loggedRequest->put('/admin/acl-user-permission/'.$this->user->id, [ 56 | 'userPermissions' => [$this->permission2->id], 57 | ]); 58 | 59 | $response->assertRedirect('/admin/user'); 60 | 61 | $userPermissions = (new GetUserPermissions)->run($this->user->id); 62 | 63 | $this->assertCount(1, $userPermissions); 64 | $this->assertEquals($this->permission2->id, $userPermissions[0]['id']); 65 | }); 66 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Tests/Role/RolePermissionTest.php: -------------------------------------------------------------------------------- 1 | role = Role::create(['name' => 'root']); 14 | $this->user = User::factory()->create(); 15 | 16 | $this->permission = Permission::create(['name' => 'first']); 17 | $this->permission2 = Permission::create(['name' => 'second']); 18 | 19 | $this->role->syncPermissions([$this->permission->id]); 20 | $this->user->assignRole($this->role); 21 | 22 | $this->loggedRequest = $this->actingAs($this->user); 23 | }); 24 | 25 | test('role permissions can be rendered', function () { 26 | $response = $this->loggedRequest->get('/admin/acl-role-permission/'.$this->role->id.'/edit'); 27 | 28 | $response->assertStatus(200); 29 | 30 | $response->assertInertia( 31 | fn (Assert $page) => $page 32 | ->component('AclRolePermission/RolePermissionForm') 33 | ->has( 34 | 'role', 35 | fn (Assert $page) => $page 36 | ->where('id', $this->role->id) 37 | ->etc() 38 | ) 39 | ->has( 40 | 'role.permissions', 41 | 1, 42 | fn (Assert $page) => $page 43 | ->where('id', $this->permission->id) 44 | ->where('name', $this->permission->name) 45 | ) 46 | ->has( 47 | 'permissions', 48 | 2 49 | ) 50 | ); 51 | }); 52 | 53 | test('role permissions can be updated', function () { 54 | $response = $this->loggedRequest->put('/admin/acl-role-permission/'.$this->role->id, [ 55 | 'rolePermissions' => [$this->permission2->id], 56 | ]); 57 | 58 | $response->assertRedirect('/admin/acl-role'); 59 | 60 | $role = Role::with(['permissions' => function ($q) { 61 | $q->get(['id', 'name']); 62 | }])->findOrFail($this->role->id); 63 | 64 | $this->assertCount(1, $role->permissions); 65 | $this->assertEquals($this->permission2->id, $role->permissions->first()->id); 66 | }); 67 | -------------------------------------------------------------------------------- /stubs/modules/Acl/Tests/Role/UserRoleTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(); 13 | $this->loggedRequest = $this->actingAs($this->user); 14 | 15 | $this->role = Role::create(['name' => 'root', 'guard_name' => 'user']); 16 | $this->role2 = Role::create(['name' => 'second', 'guard_name' => 'user']); 17 | 18 | $this->user->syncRoles([$this->role->id]); 19 | }); 20 | 21 | test('user roles can be rendered', function () { 22 | $response = $this->loggedRequest->get('/admin/acl-user-role/'.$this->user->id.'/edit'); 23 | 24 | $response->assertStatus(200); 25 | 26 | $response->assertInertia( 27 | fn (Assert $page) => $page 28 | ->component('AclUserRole/UserRoleForm') 29 | ->has( 30 | 'user', 31 | fn (Assert $page) => $page 32 | ->where('id', $this->user->id) 33 | ->etc() 34 | ) 35 | ->has( 36 | 'user.roles', 37 | 1, 38 | fn (Assert $page) => $page 39 | ->where('id', $this->role->id) 40 | ->where('name', $this->role->name) 41 | ) 42 | ->has( 43 | 'roles', 44 | 2 45 | ) 46 | ); 47 | }); 48 | 49 | test('user roles can be updated', function () { 50 | $response = $this->loggedRequest->put('/admin/acl-user-role/'.$this->user->id, [ 51 | 'userRoles' => [$this->role2->id], 52 | ]); 53 | 54 | $response->assertRedirect('/admin/user'); 55 | 56 | $user = User::with(['roles' => function ($q) { 57 | $q->get(['id', 'name']); 58 | }])->findOrFail($this->user->id); 59 | 60 | $this->assertCount(1, $user->roles); 61 | $this->assertFalse($user->hasRole($this->role->name)); 62 | $this->assertTrue($user->hasRole($this->role2->name)); 63 | }); 64 | -------------------------------------------------------------------------------- /stubs/modules/Acl/config/config.php: -------------------------------------------------------------------------------- 1 | [ 6 | // Menu Principal: Dashboard 7 | '1' => 'Menu Principal: Dashboard', 8 | 9 | // Menu Principal: Controles de Acesso 10 | '2' => 'Menu Principal: Controles de Acesso', 11 | '3' => 'Menu Principal: Controles de Acesso: Usuários - Listar', 12 | '4' => 'Menu Principal: Controles de Acesso: Permissões de Acesso - Listar', 13 | '5' => 'Menu Principal: Controles de Acesso: Perfis de Acesso - Listar', 14 | ], 15 | 16 | 'adminPermissions' => [ 17 | '1', '2', '3', '4', '5', 18 | ], 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/AdminAuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/views', 'admin-auth'); 26 | parent::boot(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/Http/Controllers/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | authenticate(); 32 | 33 | $request->session()->regenerate(); 34 | 35 | return redirect()->intended(route(config('modular.default-logged-route'))); 36 | } 37 | 38 | /** 39 | * Destroy an authenticated session. 40 | * 41 | * @return \Illuminate\Http\RedirectResponse 42 | */ 43 | public function logout(Request $request) 44 | { 45 | Auth::guard('user')->logout(); 46 | 47 | $request->session()->invalidate(); 48 | 49 | $request->session()->regenerateToken(); 50 | 51 | return Inertia::location(config('modular.login-url')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/Http/Controllers/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request->email, 24 | 'token' => $request->route('token'), 25 | ]); 26 | } 27 | 28 | /** 29 | * Handle an incoming new password request. 30 | * 31 | * @return \Illuminate\Http\RedirectResponse 32 | * 33 | * @throws \Illuminate\Validation\ValidationException 34 | */ 35 | public function store(Request $request) 36 | { 37 | $request->validate([ 38 | 'token' => 'required', 39 | 'email' => 'required|email', 40 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 41 | ]); 42 | 43 | // Here we will attempt to reset the user's password. If it is successful we 44 | // will update the password on an actual user model and persist it to the 45 | // database. Otherwise we will parse the error and return the response. 46 | $status = Password::broker('usersModule')->reset( 47 | $request->only('email', 'password', 'password_confirmation', 'token'), 48 | function ($user) use ($request) { 49 | $user->forceFill([ 50 | 'password' => Hash::make($request->password), 51 | 'remember_token' => Str::random(60), 52 | ])->save(); 53 | 54 | event(new PasswordReset($user)); 55 | } 56 | ); 57 | 58 | // If the password was successfully reset, we will redirect the user back to 59 | // the application's home authenticated view. If there is an error we can 60 | // redirect them back to where they came from with their error message. 61 | return $status == Password::broker('usersModule')::PASSWORD_RESET 62 | ? redirect()->route('adminAuth.loginForm')->with('success', 'Password updated') 63 | : back()->withInput($request->only('email')) 64 | ->withErrors(['email' => __($status)]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/Http/Controllers/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | validate([ 31 | 'email' => 'required|email', 32 | ]); 33 | 34 | // We will send the password reset link to this user. Once we have attempted 35 | // to send the link, we will examine the response then see the message we 36 | // need to show to the user. Finally, we'll send out a proper response. 37 | $status = Password::broker('usersModule')->sendResetLink( 38 | $request->only('email') 39 | ); 40 | 41 | return $status == Password::broker('usersModule')::RESET_LINK_SENT 42 | ? back()->with('success', __($status)) 43 | : back()->withInput($request->only('email')) 44 | ->withErrors(['email' => __($status)]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/Http/Middleware/UserAuth.php: -------------------------------------------------------------------------------- 1 | guest()) { 20 | return redirect()->route('adminAuth.loginForm') 21 | ->withErrors(['email' => 'Session expired, please login again.']); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/Notifications/ResetPassword.php: -------------------------------------------------------------------------------- 1 | $this->token, 36 | 'email' => $notifiable->getEmailForPasswordReset(), 37 | ]; 38 | 39 | return (new MailMessage)->markdown( 40 | 'admin-auth::emails.reset-password', 41 | [ 42 | 'url' => url(route('adminAuth.resetPasswordForm', $params, false)), 43 | ] 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/Tests/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get($loginRoute); 13 | 14 | $response->assertStatus(200); 15 | }); 16 | 17 | test('users can authenticate using the login screen', function () { 18 | $user = User::factory()->create(); 19 | 20 | $response = $this->post('/admin-auth/login', [ 21 | 'email' => $user->email, 22 | 'password' => 'password', 23 | ]); 24 | 25 | $this->assertAuthenticated(); 26 | $response->assertRedirect('/admin/dashboard'); 27 | }); 28 | 29 | test('users can not authenticate with invalid password', function () { 30 | $user = User::factory()->create(); 31 | 32 | $this->post('/login', [ 33 | 'email' => $user->email, 34 | 'password' => 'wrong-password', 35 | ]); 36 | 37 | $this->assertGuest(); 38 | }); 39 | 40 | test('users can logout', function () { 41 | $loginRoute = config('modular.login-url'); 42 | $user = User::factory()->create(); 43 | 44 | $response = $this->actingAs($user)->get('/admin-auth/logout'); 45 | 46 | $this->assertGuest(); 47 | 48 | $response->assertRedirect($loginRoute); 49 | }); 50 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/Tests/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/admin-auth/forgot-password'); 13 | 14 | $response->assertStatus(200); 15 | }); 16 | 17 | test('reset password link can be requested', function () { 18 | Notification::fake(); 19 | 20 | $user = User::factory()->create(); 21 | 22 | $this->post('/admin-auth/send-reset-link-email', ['email' => $user->email]); 23 | 24 | Notification::assertSentTo($user, AdminAuthResetPassword::class); 25 | }); 26 | 27 | test('reset password screen can be rendered', function () { 28 | Notification::fake(); 29 | 30 | $user = User::factory()->create(); 31 | 32 | $this->post('/admin-auth/send-reset-link-email', ['email' => $user->email]); 33 | 34 | Notification::assertSentTo($user, AdminAuthResetPassword::class, function ($notification) { 35 | $response = $this->get('/admin-auth/reset-password/'.$notification->token); 36 | 37 | $response->assertStatus(200); 38 | 39 | return true; 40 | }); 41 | }); 42 | 43 | test('password can be reset with valid token', function () { 44 | Notification::fake(); 45 | 46 | $user = User::factory()->create(); 47 | 48 | $this->post('/admin-auth/send-reset-link-email', ['email' => $user->email]); 49 | 50 | Notification::assertSentTo($user, AdminAuthResetPassword::class, function ($notification) use ($user) { 51 | $response = $this->post('/admin-auth/reset-password/', [ 52 | 'token' => $notification->token, 53 | 'email' => $user->email, 54 | 'password' => 'password', 55 | 'password_confirmation' => 'password', 56 | ]); 57 | 58 | $response->assertSessionHasNoErrors(); 59 | 60 | return true; 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/routes/site.php: -------------------------------------------------------------------------------- 1 | name('adminAuth.loginForm'); 11 | 12 | Route::post('/admin-auth/login', [ 13 | AuthenticatedSessionController::class, 'login', 14 | ])->name('adminAuth.login'); 15 | 16 | Route::get('/admin-auth/logout', [ 17 | AuthenticatedSessionController::class, 'logout', 18 | ])->name('adminAuth.logout'); 19 | 20 | // form to receive the email that contains the link to reset password 21 | Route::get('/admin-auth/forgot-password', [ 22 | PasswordResetLinkController::class, 'forgotPasswordForm', 23 | ])->name('adminAuth.forgotPassword'); 24 | 25 | Route::post('/admin-auth/send-reset-link-email', [ 26 | PasswordResetLinkController::class, 'sendResetLinkEmail', 27 | ])->name('adminAuth.sendResetLinkEmail'); 28 | 29 | // password reset form 30 | Route::get('/admin-auth/reset-password/{token}', [ 31 | NewPasswordController::class, 'resetPasswordForm', 32 | ])->name('adminAuth.resetPasswordForm'); 33 | 34 | Route::post('/admin-auth/reset-password', [ 35 | NewPasswordController::class, 'store', 36 | ])->name('adminAuth.resetPassword'); 37 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/views/emails/reset-password.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # Forgot your password? 3 | 4 | Here is your password reset link. 5 | 6 | @component('mail::button', ['url' => $url]) 7 | Reset Password 8 | @endcomponent 9 | 10 | Thanks,
11 | {{ config('app.name') }} 12 | @endcomponent -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/views/forgot-password-form.blade.php: -------------------------------------------------------------------------------- 1 | @extends('admin-auth::layouts.master') 2 | 3 | @section('content') 4 | 5 |
6 |
7 |
8 |

9 | Esqueceu sua senha? 10 |

11 |
12 | 13 | @if (session('status')) 14 | 17 | @endif 18 | 19 | @if ($errors->any()) 20 |

21 | Ooops, algo deu errado 22 |

23 | 28 | @endif 29 | 30 |
31 | 32 | @csrf 33 | 34 |
35 | 39 | 40 |
41 | 42 |
43 | 47 |
48 | 49 |
50 |
51 |
52 | 53 | @endsection -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/views/layouts/master.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{-- used in axios requests if needed --}} 10 | 11 | 12 | Adm 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @yield('content') 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /stubs/modules/AdminAuth/views/login-form.blade.php: -------------------------------------------------------------------------------- 1 | @extends('admin-auth::layouts.master') 2 | 3 | @section('content') 4 | 5 |
6 | 7 | @if(session('passwordResetMessage')) 8 |
9 | {{ session('passwordResetMessage') }} 10 |
11 | @endif 12 | 13 | 14 | 15 |
16 | 17 | @endsection -------------------------------------------------------------------------------- /stubs/modules/Dashboard/DashboardServiceProvider.php: -------------------------------------------------------------------------------- 1 | User::count(), 22 | 'permissions' => Permission::count(), 23 | 'roles' => Role::count(), 24 | ]; 25 | 26 | return Inertia::render('Dashboard/DashboardIndex', [ 27 | 'count' => $count, 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/modules/Dashboard/Tests/DashboardTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(); 12 | $this->loggedRequest = $this->actingAs($this->user); 13 | }); 14 | 15 | it('can render the dashboard page', function () { 16 | $dashboardUrl = route(config('modular.default-logged-route')); 17 | $response = $this->loggedRequest->get($dashboardUrl); 18 | 19 | $response->assertStatus(200); 20 | 21 | $response->assertInertia( 22 | fn (Assert $page) => $page 23 | ->component('Dashboard/DashboardIndex') 24 | ->has( 25 | 'auth.user', 26 | fn (Assert $page) => $page 27 | ->where('id', $this->user->id) 28 | ->where('name', $this->user->name) 29 | ->etc() 30 | ) 31 | ->has( 32 | 'auth.permissions', 33 | ) 34 | ->has( 35 | 'errors', 36 | ) 37 | ->has( 38 | 'flash', 39 | 2, 40 | ) 41 | ->has( 42 | 'ziggy.routes', 43 | ) 44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /stubs/modules/Dashboard/routes/app.php: -------------------------------------------------------------------------------- 1 | name('dashboard.index'); 9 | -------------------------------------------------------------------------------- /stubs/modules/Support/BaseServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapAppRoutes(); 25 | 26 | $this->mapSiteRoutes(); 27 | 28 | $this->mapApiRoutes(); 29 | } 30 | 31 | /** 32 | * Define the "web" routes, that must be authenticated for the application. 33 | * These routes all receive session state, CSRF protection, etc. 34 | */ 35 | protected function mapAppRoutes() 36 | { 37 | Route::prefix('admin') 38 | ->middleware(['web', 'auth.user']) 39 | ->group(function ($router) { 40 | $routesPath = $this->getCurrentDir().'/routes/app.php'; 41 | 42 | if (file_exists($routesPath)) { 43 | require $routesPath; 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * Define the "web" routes for the application. 50 | * These routes all receive session state, CSRF protection, etc. 51 | */ 52 | protected function mapSiteRoutes() 53 | { 54 | Route::middleware(['web']) 55 | ->group(function ($router) { 56 | $routesPath = $this->getCurrentDir().'/routes/site.php'; 57 | 58 | if (file_exists($routesPath)) { 59 | require $routesPath; 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * Define the "api" routes for the application. 66 | * These routes are typically stateless. 67 | */ 68 | protected function mapApiRoutes() 69 | { 70 | Route::prefix('api') 71 | ->middleware(['auth:sanctum']) 72 | ->group(function ($router) { 73 | $routesPath = $this->getCurrentDir().'/routes/api.php'; 74 | 75 | if (file_exists($routesPath)) { 76 | require $routesPath; 77 | } 78 | }); 79 | } 80 | 81 | /** 82 | * Returns the module directory in context. 83 | */ 84 | protected function getCurrentDir(): string 85 | { 86 | $classInfo = new ReflectionClass($this); 87 | 88 | return dirname($classInfo->getFileName()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /stubs/modules/Support/Http/Controllers/AppController.php: -------------------------------------------------------------------------------- 1 | middleware('auth.user'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /stubs/modules/Support/Http/Requests/JsonRequest.php: -------------------------------------------------------------------------------- 1 | json([ 19 | 'errors' => $validator->errors(), 20 | ], 422)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /stubs/modules/Support/Http/Requests/Request.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('name')->nullable(); 16 | $table->timestamps(); 17 | }); 18 | }); 19 | 20 | it('logs the activity for the model', function () { 21 | $model = new class extends Model 22 | { 23 | use ActivityLog; 24 | 25 | protected $table = 'items'; 26 | 27 | protected $guarded = []; 28 | }; 29 | 30 | $item1 = $model->create(['name' => 'Item 1']); 31 | 32 | $item1->delete(); 33 | 34 | $this->assertDatabaseHas('activity_log', [ 35 | 'subject_id' => $item1->id, 36 | 'subject_type' => get_class($item1), 37 | 'description' => 'deleted', 38 | ]); 39 | }); 40 | -------------------------------------------------------------------------------- /stubs/modules/Support/Tests/EditorImageTraitTest.php: -------------------------------------------------------------------------------- 1 | file = UploadedFile::fake()->image('A nice file.jpg'); 15 | }); 16 | 17 | it('can upload an image to the default editor media file path', function () { 18 | $result = $this->uploadImage($this->file, 'original'); 19 | 20 | expect($result)->toHaveKeys(['fileName', 'filePath', 'fileUrl', 'readableName']); 21 | expect($result['readableName'])->toBe('A nice file'); 22 | assertFileExists($result['filePath']); 23 | 24 | (new Filesystem)->delete($result['filePath']); 25 | }); 26 | -------------------------------------------------------------------------------- /stubs/modules/Support/Tests/FileNameGeneratorTraitTest.php: -------------------------------------------------------------------------------- 1 | image('A nice file.jpg'); 12 | 13 | $readableName = $this->getReadableName($file); 14 | $fileName = $this->getFileName($file, $readableName, 'original'); 15 | 16 | expect($fileName)->toBe('a-nice-file.jpg'); 17 | }); 18 | -------------------------------------------------------------------------------- /stubs/modules/Support/Tests/SearchableTraitTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create([ 12 | 'name' => 'John Doe', 13 | 'email' => 'doe@gmail.com', 14 | 'password' => 'secret', 15 | ]); 16 | }); 17 | 18 | it('returns empty collection if no result is found', function () { 19 | $result = User::search('name,email', 'Jane')->get(); 20 | 21 | expect($result)->toHaveCount(0); 22 | }); 23 | 24 | it('can search a single database column', function () { 25 | $stringToSearch = 'John'; 26 | 27 | $result = User::search('name', $stringToSearch)->get(); 28 | 29 | expect($result)->toHaveCount(1); 30 | expect($result->first()->name)->toBe('John Doe'); 31 | }); 32 | 33 | it('can search multiple database columns', function () { 34 | $stringToSearch = 'doe'; 35 | 36 | $result = User::search('name,email', $stringToSearch)->get(); 37 | 38 | expect($result)->toHaveCount(1); 39 | expect($result->first()->name)->toBe('John Doe'); 40 | }); 41 | -------------------------------------------------------------------------------- /stubs/modules/Support/Tests/UpdateOrderTraitTest.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('name')->nullable(); 16 | $table->unsignedTinyInteger('order')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | }); 20 | 21 | it('can update the order of the model items', function () { 22 | $model = new class extends Model 23 | { 24 | use UpdateOrder; 25 | 26 | protected $table = 'items'; 27 | 28 | protected $guarded = []; 29 | }; 30 | 31 | $item1 = $model->create(['name' => 'Item 1', 'order' => 0]); 32 | $item2 = $model->create(['name' => 'Item 2', 'order' => 1]); 33 | 34 | $newOrder = [ 35 | ['id' => $item2->id], 36 | ['id' => $item1->id], 37 | ]; 38 | 39 | $model->updateOrder($newOrder); 40 | 41 | $this->assertEquals(0, $model->find($item2->id)->order); 42 | $this->assertEquals(1, $model->find($item1->id)->order); 43 | }); 44 | -------------------------------------------------------------------------------- /stubs/modules/Support/Tests/UploadFileTraitTest.php: -------------------------------------------------------------------------------- 1 | deleteDirectory(storage_path('user-files')); 12 | }); 13 | 14 | afterAll(function () { 15 | (new Filesystem)->deleteDirectory(storage_path('user-files')); 16 | }); 17 | 18 | it('can handle requests without uploads', function () { 19 | $result = $this->uploadFile(''); 20 | 21 | expect($result)->toBe([]); 22 | }); 23 | 24 | it('can handle requests with wrong inputs', function () { 25 | request('file', 'not an uploaded file'); 26 | 27 | $result = $this->uploadFile('file'); 28 | 29 | expect($result)->toBe([]); 30 | }); 31 | -------------------------------------------------------------------------------- /stubs/modules/Support/Traits/ActivityLog.php: -------------------------------------------------------------------------------- 1 | logAll() 18 | ->logOnlyDirty() 19 | ->useLogName('system'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /stubs/modules/Support/Traits/EditorImage.php: -------------------------------------------------------------------------------- 1 | all(), [ 16 | 'file' => $this->getUploadImageValidationRules(), 17 | ]); 18 | 19 | if ($validator->fails()) { 20 | return response()->json(['errors' => $validator->errors()->all()], 422); 21 | } 22 | 23 | $fileAttributes = $this->uploadImage(request()->file('file')); 24 | 25 | return response()->json($fileAttributes); 26 | } 27 | 28 | protected function uploadImage(UploadedFile $file): array 29 | { 30 | $readableName = $this->getReadableName($file); 31 | $fileName = $this->getFileName($file, $readableName, $this->getUploadImageNameStrategy()); 32 | 33 | $file->storeAs( 34 | $this->getUploadImagePath(), 35 | $fileName, 36 | 'public', 37 | ); 38 | 39 | return [ 40 | 'fileName' => $fileName, 41 | 'filePath' => storage_path("app/public/{$this->getUploadImagePath()}/{$fileName}"), 42 | 'fileUrl' => asset("storage/{$this->getUploadImagePath()}/{$fileName}"), 43 | 'readableName' => $readableName, 44 | ]; 45 | } 46 | 47 | private function getUploadImageValidationRules(): string 48 | { 49 | return property_exists($this, 'uploadImageValidationRules') ? $this->uploadImageValidationRules : 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048'; 50 | } 51 | 52 | private function getUploadImagePath(): string 53 | { 54 | return property_exists($this, 'uploadImagePath') ? $this->uploadImagePath : 'editor-files'; 55 | } 56 | 57 | private function getUploadImageNameStrategy(): string 58 | { 59 | return property_exists($this, 'uploadImageNameStrategy') ? $this->uploadImageNameStrategy : 'originalUUID'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /stubs/modules/Support/Traits/FileNameGenerator.php: -------------------------------------------------------------------------------- 1 | getClientOriginalExtension(); 14 | } 15 | 16 | if ($nameStrategy == 'originalUUID') { 17 | return Str::slug($readableName).'-'.Str::uuid().'.'.$file->getClientOriginalExtension(); 18 | } 19 | 20 | if ($nameStrategy == 'hash') { 21 | return $file->hashName(); 22 | } 23 | } 24 | 25 | private function getReadableName(UploadedFile $file): string 26 | { 27 | return str_replace('.'.$file->getClientOriginalExtension(), '', $file->getClientOriginalName()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stubs/modules/Support/Traits/Searchable.php: -------------------------------------------------------------------------------- 1 | when($searchTerm, function ($query) use ($columns, $searchTerm) { 24 | $query->where(function ($query) use ($columns, $searchTerm) { 25 | foreach ($columns as $index => $column) { 26 | // Use where for the first column to start the group, then orWhere for subsequent columns 27 | if ($index === 0) { 28 | $query->where($column, 'like', "%{$searchTerm}%"); 29 | } else { 30 | $query->orWhere($column, 'like', "%{$searchTerm}%"); 31 | } 32 | } 33 | }); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /stubs/modules/Support/Traits/UpdateOrder.php: -------------------------------------------------------------------------------- 1 | $item) { 15 | if (is_array($item)) { 16 | $this->where('id', $item['id']) 17 | ->update([$this->getOrderFieldName() => $index]); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /stubs/modules/Support/Traits/UploadFile.php: -------------------------------------------------------------------------------- 1 | file($inputFileName); 14 | 15 | if (! $file or ! $file instanceof UploadedFile) { 16 | return []; 17 | } 18 | 19 | $readableName = $this->getReadableName($file); 20 | $fileName = $this->getFileName($file, $readableName, $nameStrategy); 21 | 22 | $file->storeAs( 23 | $path, 24 | $fileName, 25 | $disc, 26 | ); 27 | 28 | return [$inputFileName => $fileName]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/modules/Support/Validators/recaptcha.php: -------------------------------------------------------------------------------- 1 | post('https://www.google.com/recaptcha/api/siteverify', [ 9 | 'secret' => config('services.recaptcha.secret_key'), 10 | 'response' => $value, 11 | 'remoteip' => request()->ip(), 12 | ]); 13 | 14 | $body = $response->json(); 15 | 16 | return $body['success']; 17 | }); 18 | -------------------------------------------------------------------------------- /stubs/modules/Support/Validators/required_editor.php: -------------------------------------------------------------------------------- 1 |

') { 7 | return true; 8 | } 9 | 10 | return false; 11 | }, trans('modular::validation.required_editor')); 12 | -------------------------------------------------------------------------------- /stubs/modules/Support/helpers.php: -------------------------------------------------------------------------------- 1 | fake()->name(), 17 | 'email' => fake()->unique()->safeEmail(), 18 | 'email_verified_at' => now(), 19 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 20 | 'remember_token' => Str::random(10), 21 | ]; 22 | } 23 | 24 | /** 25 | * Indicate that the model's email address should be unverified. 26 | */ 27 | public function unverified(): static 28 | { 29 | return $this->state(fn (array $attributes) => [ 30 | 'email_verified_at' => null, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stubs/modules/User/Database/Migrations/2024_01_25_000000_add_custom_fields_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('profile_type')->after('remember_token')->nullable(); 15 | $table->unsignedBigInteger('profile_id')->after('profile_type')->nullable(); 16 | $table->timestamp('deleted_at')->nullable()->after('updated_at'); 17 | }); 18 | } 19 | 20 | public function down(): void 21 | { 22 | Schema::table('users', function (Blueprint $table) { 23 | $table->dropColumn('profile_type'); 24 | $table->dropColumn('profile_id'); 25 | $table->dropColumn('deleted_at'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /stubs/modules/User/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | search(request('searchContext'), request('searchTerm')) 15 | ->paginate(request('rowsPerPage', 10)) 16 | ->withQueryString() 17 | ->through(fn ($user) => [ 18 | 'id' => $user->id, 19 | 'name' => $user->name, 20 | 'email' => $user->email, 21 | 'created_at' => $user->created_at->format('d/m/Y H:i').'h', 22 | ]); 23 | 24 | return inertia('User/UserIndex', [ 25 | 'users' => $users, 26 | ]); 27 | } 28 | 29 | public function create() 30 | { 31 | return inertia('User/UserForm'); 32 | } 33 | 34 | public function store(UserValidate $request) 35 | { 36 | User::create($request->validated()); 37 | 38 | return redirect()->route('user.index') 39 | ->with('success', 'User created'); 40 | } 41 | 42 | public function edit($id) 43 | { 44 | $user = User::select('id', 'name', 'email')->find($id); 45 | 46 | return inertia('User/UserForm', [ 47 | 'user' => $user, 48 | ]); 49 | } 50 | 51 | public function update(UserValidate $request, $id) 52 | { 53 | $user = User::findOrFail($id); 54 | 55 | $params = $request->validated(); 56 | 57 | if (empty($params['password'])) { 58 | unset($params['password']); 59 | } 60 | 61 | $user->update($params); 62 | 63 | return redirect()->route('user.index') 64 | ->with('success', 'User updated'); 65 | } 66 | 67 | public function destroy($id) 68 | { 69 | User::findOrFail($id)->delete(); 70 | 71 | return redirect()->route('user.index') 72 | ->with('success', 'User deleted'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /stubs/modules/User/Http/Requests/UserValidate.php: -------------------------------------------------------------------------------- 1 | 'required', 19 | 'email' => 'required|email', 20 | 'password' => $this->passwordRules(), 21 | 'profile_type' => 'string', 22 | 'profile_id' => 'integer|numeric', 23 | ]; 24 | } 25 | 26 | private function passwordRules() 27 | { 28 | $rules = [Password::min(8)]; 29 | 30 | if (request()->isMethod('post')) { 31 | array_unshift($rules, ['required']); 32 | } 33 | 34 | if (request()->isMethod('put') and request()->isEmptyString('password')) { 35 | return []; 36 | } 37 | 38 | return $rules; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /stubs/modules/User/Models/User.php: -------------------------------------------------------------------------------- 1 | 'datetime', 56 | ]; 57 | 58 | /** 59 | * Overwrites the method from Authenticatable/CanResetPasswordContract. 60 | * 61 | * @param string $token 62 | */ 63 | public function sendPasswordResetNotification($token) 64 | { 65 | $this->notify(new ResetPassword($token)); 66 | } 67 | 68 | public function profile() 69 | { 70 | return $this->morphTo(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /stubs/modules/User/Observers/UserObserver.php: -------------------------------------------------------------------------------- 1 | filled('password')) { 13 | $user->password = Hash::make(request('password')); 14 | } 15 | } 16 | 17 | public function created(User $user) 18 | { 19 | if (! $user->profile_type) { 20 | $user->profile_type = 'user'; 21 | $user->profile_id = $user->id; 22 | $user->save(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /stubs/modules/User/UserServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/Database/Migrations'); 30 | 31 | Relation::morphMap([ 32 | 'user' => User::class, 33 | ]); 34 | 35 | User::observe(UserObserver::class); 36 | 37 | $this->commands([ 38 | CreateUserCommand::class, 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /stubs/modules/User/routes/app.php: -------------------------------------------------------------------------------- 1 | name('user.index'); 9 | 10 | Route::get('user/create', [ 11 | UserController::class, 'create', 12 | ])->name('user.create'); 13 | 14 | Route::get('user/{id}/edit', [ 15 | UserController::class, 'edit', 16 | ])->name('user.edit'); 17 | 18 | Route::post('user', [ 19 | UserController::class, 'store', 20 | ])->name('user.store'); 21 | 22 | Route::put('user/{id}', [ 23 | UserController::class, 'update', 24 | ])->name('user.update'); 25 | 26 | Route::delete('user/{id}', [ 27 | UserController::class, 'destroy', 28 | ])->name('user.destroy'); 29 | -------------------------------------------------------------------------------- /stubs/page-stub/Components/Component.stub: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /stubs/page-stub/Composables/Composable.stub: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default function use{{ ComposableName }}() { 4 | const title = ref('foo') 5 | 6 | const {{ composableName }} = () => { 7 | return title.value 8 | } 9 | 10 | return { {{ composableName }} } 11 | } 12 | -------------------------------------------------------------------------------- /stubs/page-stub/Form.stub: -------------------------------------------------------------------------------- 1 | 31 | 32 | 72 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Auth/AppAuthLogo.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Auth/AppAuthShell.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/DataTable/AppDataSearch.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/DataTable/AppDataTable.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/DataTable/AppDataTableData.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/DataTable/AppDataTableHead.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/DataTable/AppDataTableRow.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/DataTable/AppPaginator.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 54 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppCheckbox.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 77 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppFormErrors.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppInputDate.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppInputPassword.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 48 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppInputText.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppLabel.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppRadioButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 48 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/AppTextArea.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | 50 | 76 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/TipTap/TipTapButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Form/TipTap/TipTapDivider.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Menu/AppBreadCrumb.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Menu/AppBreadCrumbItem.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Menu/AppMenuItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Menu/AppMenuSection.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Message/AppAlert.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | 54 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Message/AppFlashMessage.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 71 | 72 | 87 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Misc/AppButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 46 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Misc/AppCard.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Misc/AppImageNotAvailable.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Misc/AppLink.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Misc/AppSectionHeader.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Misc/AppTopBar.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 71 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Overlay/AppConfirmDialog.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 74 | -------------------------------------------------------------------------------- /stubs/resources/js/Composables/useAuthCan.js: -------------------------------------------------------------------------------- 1 | import { usePage } from '@inertiajs/vue3' 2 | 3 | export default function useAuthCan() { 4 | const auth = usePage().props.auth 5 | 6 | const can = (permission) => { 7 | if (auth && auth.isRootUser) { 8 | return true 9 | } 10 | 11 | return auth && auth.permissions.includes(permission) 12 | } 13 | 14 | return { can } 15 | } 16 | -------------------------------------------------------------------------------- /stubs/resources/js/Composables/useClickOutside.js: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount, onMounted, ref } from 'vue' 2 | 3 | export default function useClickOutside(elementRef) { 4 | const isClickOutside = ref(false) 5 | 6 | const handler = (e) => { 7 | if (elementRef.value && !elementRef.value.contains(e.target)) { 8 | isClickOutside.value = true 9 | } else { 10 | isClickOutside.value = false 11 | } 12 | } 13 | 14 | onMounted(() => { 15 | document.addEventListener('click', handler) 16 | }) 17 | 18 | onBeforeUnmount(() => { 19 | document.removeEventListener('click', handler) 20 | }) 21 | 22 | return { isClickOutside } 23 | } 24 | -------------------------------------------------------------------------------- /stubs/resources/js/Composables/useDataSearch.js: -------------------------------------------------------------------------------- 1 | import { ref, watch, onMounted } from 'vue' 2 | import { router } from '@inertiajs/vue3' 3 | import debounce from '@/Utils/debounce' 4 | 5 | export default function useDataSearch( 6 | routePath, 7 | columnsToSearch, 8 | aditionalParams = {} 9 | ) { 10 | const searchTerm = ref('') 11 | 12 | const debouncedSearch = debounce((value) => { 13 | const params = { 14 | page: 1, 15 | searchContext: columnsToSearch, 16 | searchTerm: value 17 | } 18 | 19 | Object.assign(params, aditionalParams) 20 | 21 | fetchData(params) 22 | }, 500) 23 | 24 | watch(searchTerm, (value, oldValue) => { 25 | //prevent new search request on paginated results and back button navigation combined 26 | if (oldValue === '' && value.length > 1) { 27 | return 28 | } 29 | 30 | debouncedSearch(value) 31 | }) 32 | 33 | const fetchData = (params) => { 34 | router.visit(routePath, { 35 | data: params, 36 | replace: true, 37 | preserveState: true 38 | }) 39 | } 40 | 41 | const clearSearch = () => { 42 | searchTerm.value = '' 43 | const params = { page: 1 } 44 | 45 | Object.assign(params, aditionalParams) 46 | fetchData(params) 47 | } 48 | 49 | // handle back button navigation 50 | onMounted(() => { 51 | const urlParams = new URLSearchParams(window.location.search) 52 | searchTerm.value = urlParams.get('searchTerm') || '' 53 | }) 54 | 55 | return { searchTerm, clearSearch } 56 | } 57 | -------------------------------------------------------------------------------- /stubs/resources/js/Composables/useFormContext.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default function useFormContext() { 4 | const isCreate = ref() 5 | const isEdit = ref() 6 | 7 | isCreate.value = route().current().includes('.create') 8 | isEdit.value = route().current().includes('.edit') 9 | 10 | return { isCreate, isEdit } 11 | } 12 | -------------------------------------------------------------------------------- /stubs/resources/js/Composables/useFormErrors.js: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { usePage } from '@inertiajs/vue3' 3 | 4 | export default function useFormErrors() { 5 | const errors = computed(() => usePage().props.errors) 6 | 7 | const errorsFields = computed(() => Object.keys(errors.value)) 8 | 9 | return { errors, errorsFields } 10 | } 11 | -------------------------------------------------------------------------------- /stubs/resources/js/Composables/useIsMobile.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default function useIsMobile() { 4 | const isMobile = ref(false) 5 | const width = window.innerWidth 6 | 7 | if (width <= 1024) { 8 | isMobile.value = true 9 | } 10 | 11 | return { isMobile } 12 | } 13 | -------------------------------------------------------------------------------- /stubs/resources/js/Composables/useTitle.js: -------------------------------------------------------------------------------- 1 | import useFormContext from '@/Composables/useFormContext' 2 | import { computed } from 'vue' 3 | import { inject } from 'vue' 4 | 5 | export default function useTitle(sectionName) { 6 | const translate = inject('translate') 7 | 8 | const { isCreate, isEdit } = useFormContext() 9 | 10 | const title = computed(() => { 11 | let prefix = '' 12 | 13 | if (isCreate.value) { 14 | prefix = 'Create' 15 | } 16 | 17 | if (isEdit.value) { 18 | prefix = 'Edit' 19 | } 20 | 21 | // let prefix = isCreate.value ? 'Create' : 'Edit' 22 | prefix = translate(prefix) 23 | return prefix + ' ' + translate(sectionName) 24 | }) 25 | 26 | return { title } 27 | } 28 | -------------------------------------------------------------------------------- /stubs/resources/js/Configs/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // main navigation - side menu 3 | items: [ 4 | { 5 | label: 'Dashboard', 6 | permission: 'Dashboard', 7 | icon: 'ri-dashboard-line', 8 | link: route('dashboard.index') 9 | }, 10 | 11 | { 12 | label: 'Access Control List', 13 | permission: 'Acl', 14 | children: [ 15 | { 16 | label: 'Users', 17 | permission: 'Acl: User - List', 18 | icon: 'ri-user-line', 19 | link: route('user.index') 20 | }, 21 | { 22 | label: 'Permissions', 23 | permission: 'Acl: Permission - List', 24 | icon: 'ri-shield-keyhole-line', 25 | link: route('aclPermission.index') 26 | }, 27 | { 28 | label: 'Roles', 29 | permission: 'Acl: Role - List', 30 | icon: 'ri-account-box-line', 31 | link: route('aclRole.index') 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /stubs/resources/js/Layouts/AuthenticatedLayout.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 68 | 69 | 81 | -------------------------------------------------------------------------------- /stubs/resources/js/Layouts/GuestLayout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/AclPermission/PermissionForm.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 71 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/AclRole/RoleForm.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 71 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Dashboard/Components/DashboardCard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 47 | 48 | 67 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Dashboard/DashboardIndex.vue: -------------------------------------------------------------------------------- 1 | 45 | 58 | -------------------------------------------------------------------------------- /stubs/resources/js/Plugins/Translations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install: (app) => { 3 | const __ = (key, replacements = {}) => { 4 | let translation = window._translations[key] || key 5 | 6 | Object.keys(replacements).forEach((replacement) => { 7 | translation = translation.replace( 8 | `:${replacement}`, 9 | replacements[replacement] 10 | ) 11 | }) 12 | 13 | return translation 14 | } 15 | 16 | app.config.globalProperties.__ = __ 17 | 18 | app.provide('translate', __) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /stubs/resources/js/Resolvers/AppComponentsResolver.js: -------------------------------------------------------------------------------- 1 | const componentGroups = { 2 | Auth: ['AppAuthLogo', 'AppAuthShell'], 3 | DataTable: [ 4 | 'AppDataSearch', 5 | 'AppDataTable', 6 | 'AppDataTableData', 7 | 'AppDataTableHead', 8 | 'AppDataTableRow', 9 | 'AppPaginator' 10 | ], 11 | Form: [ 12 | 'AppCheckbox', 13 | 'AppCombobox', 14 | 'AppDataSearch', 15 | 'AppFormErrors', 16 | 'AppInputDate', 17 | 'AppInputFile', 18 | 'AppInputPassword', 19 | 'AppInputText', 20 | 'AppLabel', 21 | 'AppRadioButton', 22 | 'AppTextArea', 23 | 'AppTipTapEditor' 24 | ], 25 | Menu: [ 26 | 'AppBreadCrumb', 27 | 'AppBreadCrumbItem', 28 | 'AppMenu', 29 | 'AppMenuItem', 30 | 'AppMenuSection' 31 | ], 32 | Message: ['AppAlert', 'AppFlashMessage', 'AppToast', 'AppTooltip'], 33 | Misc: ['AppButton', 'AppCard', 'AppLink', 'AppSectionHeader', 'AppTopBar'], 34 | Overlay: ['AppConfirmDialog', 'AppModal', 'AppSideBar'] 35 | } 36 | 37 | export default (componentName) => { 38 | if (componentName.startsWith('App')) { 39 | for (const [group, components] of Object.entries(componentGroups)) { 40 | if (components.includes(componentName)) { 41 | return { 42 | from: `@/Components/${group}/${componentName}.vue` 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /stubs/resources/js/Utils/chunk.js: -------------------------------------------------------------------------------- 1 | const chunk = (array, size = 1) => { 2 | const length = array?.length || 0 3 | if (!length || size < 1) return [] 4 | const result = [] 5 | for (let index = 0; index < length; index += size) { 6 | result.push(array.slice(index, index + size)) 7 | } 8 | return result 9 | } 10 | 11 | export default chunk 12 | -------------------------------------------------------------------------------- /stubs/resources/js/Utils/debounce.js: -------------------------------------------------------------------------------- 1 | function debounce(func, timeout = 300) { 2 | let timer 3 | return (...args) => { 4 | window.clearTimeout(timer) 5 | timer = window.setTimeout(() => { 6 | func.apply(this, args) 7 | }, timeout) 8 | } 9 | } 10 | 11 | export default debounce 12 | -------------------------------------------------------------------------------- /stubs/resources/js/Utils/slug.js: -------------------------------------------------------------------------------- 1 | function slug(string) { 2 | return string 3 | .toLowerCase() 4 | .normalize('NFD') // Normalizes to decomposed form (NFD) 5 | .replace(/[\u0300-\u036f]/g, '') // Removes diacritics 6 | .replace(/ /g, '-') 7 | .replace(/[^\w-]+/g, '') // Existing replacements 8 | } 9 | 10 | export default slug 11 | -------------------------------------------------------------------------------- /stubs/resources/js/Utils/truncate.js: -------------------------------------------------------------------------------- 1 | const truncate = (value, length, ending = '...') => { 2 | if (!value || value.length < length) { 3 | return value 4 | } 5 | 6 | return value.substring(0, length) + ending 7 | } 8 | 9 | export default truncate 10 | -------------------------------------------------------------------------------- /stubs/resources/js/app.js: -------------------------------------------------------------------------------- 1 | import '../css/app.css' 2 | import 'remixicon/fonts/remixicon.css' 3 | 4 | import { createApp, h } from 'vue' 5 | import { createInertiaApp } from '@inertiajs/vue3' 6 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' 7 | import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/index.js' 8 | 9 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel' 10 | 11 | // global components 12 | import { Link } from '@inertiajs/vue3' 13 | import Layout from './Layouts/AuthenticatedLayout.vue' 14 | 15 | import Translations from '@/Plugins/Translations' 16 | 17 | createInertiaApp({ 18 | title: (title) => `${title} - ${appName}`, 19 | resolve: (name) => { 20 | const page = resolvePageComponent( 21 | `./Pages/${name}.vue`, 22 | import.meta.glob('./Pages/**/*.vue') 23 | ) 24 | 25 | page.then((module) => { 26 | module.default.layout = module.default.layout || Layout 27 | }) 28 | 29 | return page 30 | }, 31 | setup({ el, App, props, plugin }) { 32 | return createApp({ render: () => h(App, props) }) 33 | .use(plugin) 34 | .use(ZiggyVue, Ziggy) // eslint-disable-line no-undef 35 | .use(Translations) 36 | .component('Link', Link) 37 | .mount(el) 38 | }, 39 | progress: { 40 | color: '#3e63dd' 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /stubs/resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{-- If developing using SSL/HTTPS (uncomment the line below): Enforces loading all resources over HTTPS, upgrading requests from HTTP to HTTPS for enhanced security --}} 9 | {{-- --}} 10 | 11 | {{ config('app.name', 'Laravel') }} 12 | 13 | {{-- used by Tiptap Editor --}} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @routes 23 | @vite('resources/js/app.js') 24 | @inertiaHead 25 | 26 | 27 | 28 | @inertia 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /stubs/resources/views/components/translations.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /stubs/routes/web.php: -------------------------------------------------------------------------------- 1 | 'Hello World!', 20 | // ]); 21 | // }); 22 | -------------------------------------------------------------------------------- /stubs/site/modules/Index/Http/Controllers/IndexController.php: -------------------------------------------------------------------------------- 1 | 'required', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stubs/site/modules/Index/IndexServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/views', 'index'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /stubs/site/modules/Index/Models/Index.php: -------------------------------------------------------------------------------- 1 | withoutVite(); 9 | $response = $this->get('/'); 10 | $response->assertStatus(200); 11 | }); 12 | -------------------------------------------------------------------------------- /stubs/site/modules/Index/routes/site.php: -------------------------------------------------------------------------------- 1 | name('index.index'); 9 | -------------------------------------------------------------------------------- /stubs/site/modules/Index/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('site-layout') 2 | 3 | @section('meta-title', 'Modular: Ready to build') 4 | 5 | @section('meta-description', 'Your amazing site') 6 | 7 | @section('bodyEndScripts') 8 | @vite('resources-site/js/index-app.js') 9 | @endsection 10 | 11 | @section('content') 12 |
13 | 14 |
15 | 16 |
17 |
18 | Image 19 |
20 | 21 |

Modular: Ready to build

22 | 23 |

Your amazing site

24 | 25 | 26 |
27 | 28 |
29 | 30 |
31 | @endsection 32 | -------------------------------------------------------------------------------- /stubs/site/modules/Support/SiteController.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | From ExampleComponent.vue: Let's do it! 4 |

5 | 6 | -------------------------------------------------------------------------------- /stubs/site/resources-site/js/create-vue-app.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue/dist/vue.esm-bundler.js' 2 | //import commonComponent from './Components/common-component.vue' 3 | 4 | export const createVueApp = (additionalComponents = {}) => { 5 | const app = createApp({ 6 | components: { 7 | //commonComponent, 8 | ...additionalComponents 9 | } 10 | }) 11 | 12 | import.meta.glob(['../images/**']) 13 | 14 | return app 15 | } 16 | -------------------------------------------------------------------------------- /stubs/site/resources-site/js/index-app.js: -------------------------------------------------------------------------------- 1 | import { createVueApp } from './create-vue-app.js' 2 | import IndexExampleComponent from './Components/IndexExampleComponent.vue' 3 | 4 | createVueApp({ 5 | IndexExampleComponent 6 | }).mount('#app') 7 | -------------------------------------------------------------------------------- /stubs/site/resources-site/views/pagination/simple-tailwind.blade.php: -------------------------------------------------------------------------------- 1 | @if ($paginator->hasPages()) 2 | 25 | @endif 26 | -------------------------------------------------------------------------------- /stubs/site/resources-site/views/site-layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @yield('meta-title', config('app.name', 'Modular')) 9 | 10 | 14 | 15 | 16 | @if (app()->environment('production')) 17 | 21 | @endif 22 | 23 | 24 | 25 | 26 | 27 | @vite(['resources-site/css/site.css']) 28 | @yield('headEndScripts') 29 | 30 | 31 | 32 |
33 | @yield('content') 34 |
35 | 36 | @yield('bodyEndScripts') 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /stubs/stack-configs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "arrowParens": "always", 6 | 7 | "tailwindConfig": "tailwind.config.mjs", 8 | "plugins": ["prettier-plugin-blade", "prettier-plugin-tailwindcss"], 9 | 10 | "overrides": [ 11 | { 12 | "files": ["*.blade.php"], 13 | "options": { 14 | "parser": "blade" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /stubs/stack-configs/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.customData": [".vscode/tailwind.json"], 3 | "tailwindCSS.files.exclude": ["**/node_modules/**", "**/vendor/**"], 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "[vue]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[php]": { 10 | "editor.defaultFormatter": "open-southeners.laravel-pint" 11 | }, 12 | "[blade]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "prettier.documentSelectors": ["**/*.blade.php"] 16 | } 17 | -------------------------------------------------------------------------------- /stubs/stack-configs/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import vue from 'eslint-plugin-vue' 3 | import vueParser from 'vue-eslint-parser' 4 | import prettierConfig from 'eslint-config-prettier' 5 | import globals from 'globals' 6 | 7 | export default [ 8 | // Base ESLint recommended rules 9 | js.configs.recommended, 10 | 11 | // Prettier configuration to disable conflicting rules 12 | { 13 | rules: { 14 | ...prettierConfig.rules, 15 | 'unicode-bom': 'off' 16 | } 17 | }, 18 | 19 | // Vue plugin configuration 20 | { 21 | files: ['**/*.vue'], 22 | languageOptions: { 23 | parser: vueParser, 24 | parserOptions: { 25 | ecmaVersion: 'latest', 26 | sourceType: 'module' 27 | } 28 | }, 29 | plugins: { 30 | vue 31 | }, 32 | rules: { 33 | // Combine base and recommended Vue rules 34 | ...vue.configs.base.rules, 35 | ...vue.configs['vue3-recommended'].rules, 36 | 37 | '@stylistic/js/indent': 'off', 38 | '@stylistic/js/quotes': 'off', 39 | 40 | // Disable specific Vue rules 41 | 'vue/no-v-html': 'off', 42 | 'vue/comment-directive': 'off' 43 | 44 | // You can add other Vue-specific rules here 45 | } 46 | }, 47 | 48 | // General JavaScript rules (for .js and .vue files) 49 | { 50 | files: ['**/*.{js,vue}'], 51 | rules: { 52 | // Disable general ESLint rules 53 | // 'no-undef': 'off' 54 | } 55 | }, 56 | 57 | // Custom rules (if any) 58 | { 59 | languageOptions: { 60 | globals: { 61 | ...globals.browser, 62 | route: 'readonly', 63 | grecaptcha: 'readonly' 64 | } 65 | }, 66 | rules: { 67 | // Add your custom rules here 68 | } 69 | }, 70 | 71 | // Ignore patterns 72 | { 73 | ignores: ['node_modules/*', 'vendor/*', 'public/*'] 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /stubs/stack-configs/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"] 6 | }, 7 | "jsx": "preserve" 8 | }, 9 | "exclude": ["node_modules", "public"] 10 | } 11 | -------------------------------------------------------------------------------- /stubs/stack-configs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; -------------------------------------------------------------------------------- /stubs/stack-configs/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 4 | './storage/framework/views/*.php', 5 | './resources/views/**/*.blade.php', 6 | './resources/js/**/*.vue', 7 | './resources-site/views/**/*.blade.php', 8 | './resources-site/js/**/*.vue', 9 | './modules/**/views/**/*.blade.php' 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /stubs/stack-configs/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import tailwindcss from '@tailwindcss/vite' 3 | import laravel from 'laravel-vite-plugin' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | import Components from 'unplugin-vue-components/vite' 7 | import AppComponentsResolver from './resources/js/Resolvers/AppComponentsResolver.js' 8 | 9 | export default defineConfig({ 10 | plugins: [ 11 | tailwindcss(), 12 | laravel({ 13 | input: ['resources/js/app.js'], 14 | refresh: ['resources/**/*'] 15 | }), 16 | vue({ 17 | template: { 18 | transformAssetUrls: { 19 | base: null, 20 | includeAbsolute: false 21 | } 22 | } 23 | }), 24 | Components({ 25 | resolvers: [AppComponentsResolver] 26 | }) 27 | ], 28 | resolve: { 29 | alias: { 30 | '@resources': '/resources' 31 | } 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /stubs/tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /stubs/tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | expect()->extend('toBeOne', function () { 31 | return $this->toBe(1); 32 | }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function something() 46 | { 47 | // .. 48 | } 49 | -------------------------------------------------------------------------------- /stubs/tests/TestCase.php: -------------------------------------------------------------------------------- 1 |