├── .editorconfig ├── .env.ci ├── .env.example ├── .gitattributes ├── .gitignore ├── .prettierrc.json ├── README.md ├── app ├── Http │ ├── Controllers │ │ └── Controller.php │ └── Middleware │ │ └── HandleInertiaRequests.php ├── Models │ └── User.php └── Providers │ └── AppServiceProvider.php ├── art ├── modular-demo-screen-black.jpg ├── modular-demo-screen-white.jpg └── modular-logo.svg ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── activitylog.php ├── app.php ├── auth.php ├── cache.php ├── database.php ├── filesystems.php ├── logging.php ├── mail.php ├── modular.php ├── permission.php ├── queue.php ├── services.php ├── session.php └── view.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2025_03_28_132454_create_permission_tables.php │ ├── 2025_03_28_132455_create_activity_log_table.php │ ├── 2025_03_28_132456_add_event_column_to_activity_log_table.php │ └── 2025_03_28_132457_add_batch_uuid_column_to_activity_log_table.php └── seeders │ └── DatabaseSeeder.php ├── eslint.config.js ├── jsconfig.json ├── lang ├── en │ └── pagination.php └── vendor │ └── modular │ ├── en │ ├── en.json │ └── validation.php │ └── pt_BR │ └── pt_BR.json ├── modules ├── Acl │ ├── AclServiceProvider.php │ ├── Database │ │ └── Seeders │ │ │ ├── AclModelHasRolesSeeder.php │ │ │ ├── AclPermissionSeeder.php │ │ │ ├── AclRoleHasPermissionsSeeder.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 ├── Blog │ ├── BlogServiceProvider.php │ ├── Database │ │ ├── Factories │ │ │ ├── BlogAuthorFactory.php │ │ │ ├── BlogCategoryFactory.php │ │ │ ├── BlogPostFactory.php │ │ │ └── BlogTagFactory.php │ │ ├── Migrations │ │ │ ├── 2023_11_28_184920_create_blog_authors_table.php │ │ │ ├── 2023_11_28_184921_create_blog_categories_table.php │ │ │ ├── 2023_11_29_165237_create_blog_posts_table.php │ │ │ ├── 2024_01_29_190914_create_blog_tags_table.php │ │ │ └── 2024_01_29_191330_create_blog_posts_tags_table.php │ │ └── Seeders │ │ │ └── BlogSeeder.php │ ├── Http │ │ ├── Controllers │ │ │ ├── AuthorController.php │ │ │ ├── CategoryController.php │ │ │ ├── PostController.php │ │ │ ├── SiteArchiveController.php │ │ │ ├── SitePostController.php │ │ │ ├── SitePostSearchController.php │ │ │ ├── SiteTagController.php │ │ │ └── TagController.php │ │ └── Requests │ │ │ ├── AuthorValidate.php │ │ │ ├── CategoryValidate.php │ │ │ ├── PostValidate.php │ │ │ └── TagValidate.php │ ├── Models │ │ ├── Author.php │ │ ├── Category.php │ │ ├── Post.php │ │ └── Tag.php │ ├── Observers │ │ └── PostObserver.php │ ├── Services │ │ ├── GetAuthorOptions.php │ │ ├── GetCategoryOptions.php │ │ ├── GetTagOptions.php │ │ ├── Site │ │ │ ├── GetArchiveOptions.php │ │ │ ├── GetPostsFromArchive.php │ │ │ └── GetTagOptions.php │ │ └── SyncPostTags.php │ ├── Tests │ │ ├── AuthorTest.php │ │ ├── CategoryTest.php │ │ ├── PostTest.php │ │ ├── Services │ │ │ ├── GetAuthorOptionsTest.php │ │ │ ├── GetCategoryOptionsTest.php │ │ │ ├── GetTagOptionsTest.php │ │ │ └── SyncPostTagsTest.php │ │ └── Site │ │ │ ├── PostTest.php │ │ │ └── Services │ │ │ ├── GetArchiveOptionsTest.php │ │ │ ├── GetPostsFromArchiveTest.php │ │ │ └── GetTagOptionsTest.php │ ├── routes │ │ ├── app.php │ │ └── site.php │ └── views │ │ ├── post-index.blade.php │ │ └── post-show.blade.php ├── Dashboard │ ├── DashboardServiceProvider.php │ ├── Http │ │ └── Controllers │ │ │ └── DashboardController.php │ ├── Tests │ │ └── DashboardTest.php │ └── routes │ │ └── app.php ├── Index │ ├── Http │ │ ├── Controllers │ │ │ └── IndexController.php │ │ └── Requests │ │ │ └── IndexValidate.php │ ├── IndexServiceProvider.php │ ├── Models │ │ └── Index.php │ ├── Tests │ │ └── IndexTest.php │ ├── routes │ │ └── site.php │ └── views │ │ └── index.blade.php ├── Support │ ├── BaseServiceProvider.php │ ├── Http │ │ ├── Controllers │ │ │ ├── AppController.php │ │ │ ├── BackendController.php │ │ │ └── SiteController.php │ │ └── Requests │ │ │ ├── JsonRequest.php │ │ │ └── Request.php │ ├── Models │ │ ├── BaseModel.php │ │ └── SiteModel.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 │ └── Seeders │ │ └── UserSeeder.php │ ├── Http │ ├── Controllers │ │ └── UserController.php │ └── Requests │ │ └── UserValidate.php │ ├── Models │ └── User.php │ ├── Observers │ └── UserObserver.php │ ├── Tests │ ├── CreateUserCommandTest.php │ └── UserTest.php │ ├── UserServiceProvider.php │ └── routes │ └── app.php ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.mjs ├── public ├── .htaccess ├── favicon.ico ├── favicon.svg ├── index.php └── robots.txt ├── resources-site ├── css │ ├── blog.css │ └── site.css ├── images │ └── home-img.png ├── js │ ├── Components │ │ ├── Blog │ │ │ ├── ArchiveSelector.vue │ │ │ ├── BlogToolbar.vue │ │ │ ├── SearchInput.vue │ │ │ └── TagSelector.vue │ │ └── IndexExampleComponent.vue │ ├── blog-app.js │ ├── create-vue-app.js │ └── index-app.js └── views │ ├── pagination │ ├── simple-tailwind.blade.php │ └── tailwind.blade.php │ └── site-layout.blade.php ├── resources ├── css │ └── app.css ├── images │ ├── blog │ │ ├── 034cb604-d3d0-448b-92f1-6a255fb4653d.jpg │ │ ├── 0d2dc407-6b6f-4022-9565-1ecec79daf52.jpg │ │ ├── 153cfa68-8ca8-4712-a1c1-1bd969796ef9.jpg │ │ ├── 15dcfc3d-f8f4-470e-9477-72322e399731.jpg │ │ ├── 2348a092-55d0-4b6d-b43d-3b34280dbed9.jpg │ │ ├── 24521518-b796-445f-8e9a-59233d8d78fa.jpg │ │ ├── 2a7f2576-82a1-4f28-9f38-5a9cc216ba16.jpg │ │ ├── 3d7da554-1a6c-44f6-9f9f-2a6947446a65.jpg │ │ ├── 3fe0425a-96a4-49b5-bcde-adf75caa626d.jpg │ │ ├── 44a928b1-5148-4940-af48-2792f1151272.jpg │ │ ├── 463c0606-1382-49b3-8ba5-3a3b91c4ff09.jpg │ │ ├── 49814997-b619-4281-842f-8729f2ec151c.jpg │ │ ├── 4c798d2c-c4a6-4293-b052-98a048914215.jpg │ │ ├── 4e8d6a91-e0ef-48aa-86cd-8d18699407c3.jpg │ │ ├── 500862ad-b078-4306-9348-39edf65f6d05.jpg │ │ ├── 548857a2-c1b5-490e-b3a2-20a28ba98e0f.jpg │ │ ├── 57135dbe-7d31-4380-85cd-b78179ca2400.jpg │ │ ├── 57b76f29-dd7d-4018-b172-a06e6ef4a4cf.jpg │ │ ├── 5a888786-090c-40f1-85e0-f86895311eeb.jpg │ │ ├── 5f4ecece-6275-41b9-bd64-0e6f904a5df7.jpg │ │ ├── 5febe62d-d9bd-4054-b5a1-83b796ca9d14.jpg │ │ ├── 68255060-d85e-4081-a025-931789e6aa1d.jpg │ │ ├── 735aa22c-6aa0-4ced-9552-5494f0da4e5e.jpg │ │ ├── 75fca485-2ea3-47d0-b609-9c8bd58d3438.jpg │ │ ├── 77aa4a7a-de06-4ae6-8a1d-42322f532c53.jpg │ │ ├── 850e4903-66fe-4feb-a218-64058bb84a6a.jpg │ │ ├── 8c183760-1de3-4f6a-bef6-7241e7ebbb2f.jpg │ │ ├── 94ab2a3f-2300-4754-b458-2cc36cceac05.jpg │ │ ├── 952536c8-e74c-4089-bce4-3cc3b474a2d2.jpg │ │ ├── 9ab99b32-1f04-495e-b5a2-5fa789dedd18.jpg │ │ ├── a98ea6d4-9f8b-4897-9c63-bdafc1acd602.jpg │ │ ├── b51306e7-f8f8-49b3-9083-e3fffccad1b4.jpg │ │ ├── b8cac8c5-c3d2-4a65-8ebb-1b74059fe309.jpg │ │ ├── bac12f21-8eaa-437e-bf0c-2ae84ca2b02a.jpg │ │ ├── cd2dc303-24c8-41b3-b09e-83205a647d8f.jpg │ │ ├── cf45d6f9-0ff0-41ae-966b-a7fa50f08e33.jpg │ │ ├── d27f5f87-f8dd-47df-8b2e-5086a5ba66c1.jpg │ │ ├── da4bc233-b12b-4548-904c-b3cf70711607.jpg │ │ ├── dc44c3b1-6de3-4c47-80bb-e28de18f314b.jpg │ │ ├── e42eefc0-7f34-4645-ba21-d9c931daf08f.jpg │ │ ├── e6ebdade-56ec-4d59-9da2-cf479b7d8c46.jpg │ │ ├── e9d82a31-b7a2-46d8-9da4-48f51f4207aa.jpg │ │ ├── edb880ab-4e13-4594-b6d1-fa4125a00100.jpg │ │ ├── f0d5596c-d23a-4db7-97a5-9a298b924994.jpg │ │ ├── f5e456c2-de4b-48c2-9554-54b8b8e7bfbc.jpg │ │ ├── f6d0c648-5d59-4b96-9c44-ebdc0ba3ee38.jpg │ │ ├── faa02464-6d3e-49b9-a45c-de303c91f3d5.jpg │ │ └── fe15a944-36e4-4c0b-a634-f9e6c00df101.jpg │ └── 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 │ │ ├── Modules │ │ │ └── Blog │ │ │ │ └── AppImageNotAvailable.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 │ │ ├── BlogAuthor │ │ │ ├── AuthorForm.vue │ │ │ └── AuthorIndex.vue │ │ ├── BlogCategory │ │ │ ├── CategoryForm.vue │ │ │ ├── CategoryIndex.vue │ │ │ ├── CategoryStore.js │ │ │ └── Components │ │ │ │ ├── CategoryBody.vue │ │ │ │ ├── CategoryImage.vue │ │ │ │ ├── CategorySeo.vue │ │ │ │ └── CategoryVisibility.vue │ │ ├── BlogPost │ │ │ ├── Components │ │ │ │ ├── PostAuthor.vue │ │ │ │ ├── PostBody.vue │ │ │ │ ├── PostCategory.vue │ │ │ │ ├── PostImage.vue │ │ │ │ ├── PostPublishDate.vue │ │ │ │ ├── PostSeo.vue │ │ │ │ └── PostTags.vue │ │ │ ├── PostForm.vue │ │ │ ├── PostIndex.vue │ │ │ └── PostStore.js │ │ ├── BlogTag │ │ │ ├── TagForm.vue │ │ │ └── TagIndex.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 │ └── welcome.blade.php ├── routes ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.mjs ├── tests ├── Feature │ └── ExampleTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.ci: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=testing 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_LEVEL=debug 9 | 10 | DB_CONNECTION=sqlite 11 | DB_DATABASE=:memory: 12 | 13 | BROADCAST_DRIVER=log 14 | CACHE_DRIVER=array 15 | QUEUE_CONNECTION=sync 16 | SESSION_DRIVER=array 17 | SESSION_LIFETIME=120 -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=sqlite 12 | DB_FOREIGN_KEYS=true 13 | 14 | BROADCAST_DRIVER=log 15 | CACHE_DRIVER=file 16 | FILESYSTEM_DISK=local 17 | QUEUE_CONNECTION=sync 18 | SESSION_DRIVER=file 19 | SESSION_LIFETIME=120 20 | 21 | MEMCACHED_HOST=127.0.0.1 22 | 23 | REDIS_HOST=127.0.0.1 24 | REDIS_PASSWORD=null 25 | REDIS_PORT=6379 26 | 27 | MAIL_MAILER=smtp 28 | MAIL_HOST=mailpit 29 | MAIL_PORT=1025 30 | MAIL_USERNAME=null 31 | MAIL_PASSWORD=null 32 | MAIL_ENCRYPTION=null 33 | MAIL_FROM_ADDRESS="hello@example.com" 34 | MAIL_FROM_NAME="${APP_NAME}" 35 | 36 | AWS_ACCESS_KEY_ID= 37 | AWS_SECRET_ACCESS_KEY= 38 | AWS_DEFAULT_REGION=us-east-1 39 | AWS_BUCKET= 40 | AWS_USE_PATH_STYLE_ENDPOINT=false 41 | 42 | PUSHER_APP_ID= 43 | PUSHER_APP_KEY= 44 | PUSHER_APP_SECRET= 45 | PUSHER_HOST= 46 | PUSHER_PORT=443 47 | PUSHER_SCHEME=https 48 | PUSHER_APP_CLUSTER=mt1 49 | 50 | VITE_APP_NAME="${APP_NAME}" 51 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 52 | VITE_PUSHER_HOST="${PUSHER_HOST}" 53 | VITE_PUSHER_PORT="${PUSHER_PORT}" 54 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 55 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | Homestead.json 13 | Homestead.yaml 14 | auth.json 15 | npm-debug.log 16 | yarn-error.log 17 | /.fleet 18 | /.idea 19 | /.vscode 20 | .DS_Store -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.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 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 13 | use HasFactory, Notifiable; 14 | 15 | /** 16 | * The attributes that are mass assignable. 17 | * 18 | * @var list 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | ]; 25 | 26 | /** 27 | * The attributes that should be hidden for serialization. 28 | * 29 | * @var list 30 | */ 31 | protected $hidden = [ 32 | 'password', 33 | 'remember_token', 34 | ]; 35 | 36 | /** 37 | * Get the attributes that should be cast. 38 | * 39 | * @return array 40 | */ 41 | protected function casts(): array 42 | { 43 | return [ 44 | 'email_verified_at' => 'datetime', 45 | 'password' => 'hashed', 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 17 | 18 | exit($status); 19 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 9 | web: __DIR__.'/../routes/web.php', 10 | commands: __DIR__.'/../routes/console.php', 11 | health: '/up', 12 | ) 13 | ->withMiddleware(function (Middleware $middleware) { 14 | $middleware->alias([ 15 | 'auth.user' => \Modules\AdminAuth\Http\Middleware\UserAuth::class, 16 | ]); 17 | 18 | $middleware->web(append: [ 19 | \App\Http\Middleware\HandleInertiaRequests::class, 20 | ]); 21 | 22 | // 23 | }) 24 | ->withExceptions(function (Exceptions $exceptions) { 25 | // 26 | })->create(); 27 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('ACTIVITY_LOGGER_ENABLED', true), 9 | 10 | /* 11 | * When the clean-command is executed, all recording activities older than 12 | * the number of days specified here will be deleted. 13 | */ 14 | 'delete_records_older_than_days' => 365, 15 | 16 | /* 17 | * If no log name is passed to the activity() helper 18 | * we use this default log name. 19 | */ 20 | 'default_log_name' => 'default', 21 | 22 | /* 23 | * You can specify an auth driver here that gets user models. 24 | * If this is null we'll use the current Laravel auth driver. 25 | */ 26 | 'default_auth_driver' => null, 27 | 28 | /* 29 | * If set to true, the subject returns soft deleted models. 30 | */ 31 | 'subject_returns_soft_deleted_models' => false, 32 | 33 | /* 34 | * This model will be used to log activity. 35 | * It should implement the Spatie\Activitylog\Contracts\Activity interface 36 | * and extend Illuminate\Database\Eloquent\Model. 37 | */ 38 | 'activity_model' => \Spatie\Activitylog\Models\Activity::class, 39 | 40 | /* 41 | * This is the name of the table that will be created by the migration and 42 | * used by the Activity model shipped with this package. 43 | */ 44 | 'table_name' => env('ACTIVITY_LOGGER_TABLE_NAME', 'activity_log'), 45 | 46 | /* 47 | * This is the database connection that will be used by the migration and 48 | * the Activity model shipped with this package. In case it's not set 49 | * Laravel's database.default will be used instead. 50 | */ 51 | 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), 52 | ]; 53 | -------------------------------------------------------------------------------- /config/modular.php: -------------------------------------------------------------------------------- 1 | '/', 5 | 'default-logged-route' => 'dashboard.index', 6 | ]; 7 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | base_path('resources-site/views'), 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Compiled View Path 24 | |-------------------------------------------------------------------------- 25 | | 26 | | This option determines where all the compiled Blade templates will be 27 | | stored for your application. Typically, this is within the storage 28 | | directory. However, as usual, you are free to change this value. 29 | | 30 | */ 31 | 32 | 'compiled' => env( 33 | 'VIEW_COMPILED_PATH', 34 | realpath(storage_path('framework/views')) 35 | ), 36 | 37 | ]; 38 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /database/migrations/2025_03_28_132455_create_activity_log_table.php: -------------------------------------------------------------------------------- 1 | create(config('activitylog.table_name'), function (Blueprint $table) { 12 | $table->bigIncrements('id'); 13 | $table->string('log_name')->nullable(); 14 | $table->text('description'); 15 | $table->nullableMorphs('subject', 'subject'); 16 | $table->nullableMorphs('causer', 'causer'); 17 | $table->json('properties')->nullable(); 18 | $table->timestamps(); 19 | $table->index('log_name'); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2025_03_28_132456_add_event_column_to_activity_log_table.php: -------------------------------------------------------------------------------- 1 | table(config('activitylog.table_name'), function (Blueprint $table) { 12 | $table->string('event')->nullable()->after('subject_type'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { 19 | $table->dropColumn('event'); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/2025_03_28_132457_add_batch_uuid_column_to_activity_log_table.php: -------------------------------------------------------------------------------- 1 | table(config('activitylog.table_name'), function (Blueprint $table) { 12 | $table->uuid('batch_uuid')->nullable()->after('properties'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { 19 | $table->dropColumn('batch_uuid'); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 18 | AclRoleSeeder::class, 19 | AclPermissionSeeder::class, 20 | BlogSeeder::class, 21 | UserSeeder::class, 22 | AclModelHasRolesSeeder::class, 23 | AclRoleHasPermissionsSeeder::class, 24 | AclRoleSeeder::class, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"] 6 | }, 7 | "jsx": "preserve" 8 | }, 9 | "exclude": ["node_modules", "public"] 10 | } 11 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | 'Showing' => 'Showing', 20 | 'to' => 'to', 21 | 'of' => 'of', 22 | 'results' => 'results', 23 | 24 | ]; 25 | -------------------------------------------------------------------------------- /lang/vendor/modular/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute field is required.', 5 | ]; 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/Acl/Database/Seeders/AclModelHasRolesSeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 17 | 18 | //root 19 | DB::table('model_has_roles')->insert([ 20 | 'role_id' => 1, 21 | 'model_type' => 'user', 22 | 'model_id' => 1, 23 | ]); 24 | 25 | //content author 26 | DB::table('model_has_roles')->insert([ 27 | 'role_id' => 2, 28 | 'model_type' => 'user', 29 | 'model_id' => 2, 30 | ]); 31 | 32 | //content director 33 | DB::table('model_has_roles')->insert([ 34 | 'role_id' => 3, 35 | 'model_type' => 'user', 36 | 'model_id' => 3, 37 | ]); 38 | 39 | Schema::enableForeignKeyConstraints(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /modules/Acl/Database/Seeders/AclRoleHasPermissionsSeeder.php: -------------------------------------------------------------------------------- 1 | $contentAuthorPermissions, 38 | 3 => $contentDirectorPermissions, 39 | ]; 40 | 41 | foreach ($rolesWithPermissions as $roleId => $permissionsIds) { 42 | $role = Role::findOrFail($roleId); 43 | 44 | foreach ($permissionsIds as $permissionId) { 45 | $role->givePermissionTo((int) $permissionId); 46 | } 47 | } 48 | 49 | Schema::enableForeignKeyConstraints(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/Acl/Database/Seeders/AclRoleSeeder.php: -------------------------------------------------------------------------------- 1 | 'root', 19 | 'guard_name' => 'user', 20 | ]); 21 | 22 | Role::create([ 23 | 'name' => 'content author', 24 | 'guard_name' => 'user', 25 | ]); 26 | 27 | Role::create([ 28 | 'name' => 'content director', 29 | 'guard_name' => 'user', 30 | ]); 31 | 32 | Schema::enableForeignKeyConstraints(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/Acl/Services/ListUserPermissions.php: -------------------------------------------------------------------------------- 1 | run($userId); 12 | 13 | return Arr::pluck($userPermissions, 'name'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/AdminAuth/AdminAuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/views', 'admin-auth'); 26 | parent::boot(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /modules/Blog/BlogServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/Database/Migrations'); 18 | $this->loadViewsFrom(__DIR__.'/views', 'blog'); 19 | 20 | Post::observe(PostObserver::class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/Blog/Database/Factories/BlogAuthorFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name(), 16 | 'email' => $this->faker->unique()->safeEmail(), 17 | 'bio' => $this->faker->realTextBetween(), 18 | 'image' => $this->faker->imageUrl(), 19 | 'github_handle' => $this->faker->userName(), 20 | 'twitter_handle' => $this->faker->userName(), 21 | 'created_at' => $this->faker->dateTimeBetween('-1 year', '-3 month'), 22 | 'updated_at' => $this->faker->dateTimeBetween('-2 month', 'now'), 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/Blog/Database/Factories/BlogCategoryFactory.php: -------------------------------------------------------------------------------- 1 | faker->unique()->sentence(4); 16 | 17 | return [ 18 | 'name' => $name, 19 | 'description' => $this->faker->realText(), 20 | 'image' => $this->faker->imageUrl(), 21 | 'is_visible' => $this->faker->boolean(), 22 | 'slug' => Str::slug($name), 23 | 'meta_tag_title' => Str::limit($name, 60, ''), 24 | 'meta_tag_description' => Str::limit($name, 160, ''), 25 | 'created_at' => $this->faker->dateTimeBetween('-1 year', '-6 month'), 26 | 'updated_at' => $this->faker->dateTimeBetween('-5 month', 'now'), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/Blog/Database/Factories/BlogPostFactory.php: -------------------------------------------------------------------------------- 1 | faker->unique()->sentence(4); 16 | 17 | return [ 18 | 'title' => $title, 19 | 'slug' => Str::slug($title), 20 | 'content' => $this->faker->realText(), 21 | 'summary' => $this->faker->realText(100), 22 | 'image' => $this->faker->imageUrl(), 23 | 'meta_tag_title' => Str::limit($title, 60, ''), 24 | 'meta_tag_description' => Str::limit($title, 160, ''), 25 | 'published_at' => $this->faker->dateTimeBetween('-6 month', '+3 month'), 26 | 'created_at' => $this->faker->dateTimeBetween('-1 year', '-6 month'), 27 | 'updated_at' => $this->faker->dateTimeBetween('-5 month', 'now'), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/Blog/Database/Factories/BlogTagFactory.php: -------------------------------------------------------------------------------- 1 | fake()->name(), 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/Blog/Database/Migrations/2023_11_28_184920_create_blog_authors_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->string('image')->nullable(); 19 | $table->longText('bio')->nullable(); 20 | $table->string('github_handle')->nullable(); 21 | $table->string('twitter_handle')->nullable(); 22 | $table->timestamps(); 23 | $table->softDeletes(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('blog_authors'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /modules/Blog/Database/Migrations/2023_11_28_184921_create_blog_categories_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->longText('description')->nullable(); 18 | $table->string('image')->nullable(); 19 | $table->boolean('is_visible')->default(false); 20 | $table->string('slug')->unique(); 21 | $table->string('meta_tag_title', 60)->nullable(); 22 | $table->string('meta_tag_description', 160)->nullable(); 23 | $table->timestamps(); 24 | $table->softDeletes(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('blog_categories'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /modules/Blog/Database/Migrations/2023_11_29_165237_create_blog_posts_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('blog_author_id')->nullable(); 17 | $table->foreignId('blog_category_id')->nullable(); 18 | $table->string('title'); 19 | $table->string('slug')->unique(); 20 | $table->text('summary')->nullable(); 21 | $table->longText('content'); 22 | $table->string('image')->nullable(); 23 | $table->string('meta_tag_title', 60)->nullable(); 24 | $table->string('meta_tag_description', 160)->nullable(); 25 | $table->date('published_at')->nullable(); 26 | $table->timestamps(); 27 | $table->softDeletes(); 28 | 29 | $table->foreign('blog_author_id') 30 | ->references('id') 31 | ->on('blog_authors') 32 | ->cascadeOnUpdate() 33 | ->nullOnDelete(); 34 | 35 | $table->foreign('blog_category_id') 36 | ->references('id') 37 | ->on('blog_categories') 38 | ->cascadeOnUpdate() 39 | ->nullOnDelete(); 40 | }); 41 | } 42 | 43 | /** 44 | * Reverse the migrations. 45 | */ 46 | public function down(): void 47 | { 48 | Schema::dropIfExists('blog_posts'); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /modules/Blog/Database/Migrations/2024_01_29_190914_create_blog_tags_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('slug')->unique(); 18 | $table->timestamps(); 19 | $table->softDeletes(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('blog_tags'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /modules/Blog/Database/Migrations/2024_01_29_191330_create_blog_posts_tags_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('blog_post_id'); 17 | $table->foreignId('blog_tag_id'); 18 | $table->timestamps(); 19 | 20 | $table->foreign('blog_post_id') 21 | ->references('id') 22 | ->on('blog_posts') 23 | ->cascadeOnUpdate() 24 | ->cascadeOnDelete(); 25 | 26 | $table->foreign('blog_tag_id') 27 | ->references('id') 28 | ->on('blog_tags') 29 | ->cascadeOnUpdate() 30 | ->cascadeOnDelete(); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | */ 37 | public function down(): void 38 | { 39 | Schema::dropIfExists('blog_posts_tags'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /modules/Blog/Http/Controllers/SiteArchiveController.php: -------------------------------------------------------------------------------- 1 | get($archiveDate); 16 | 17 | $archiveOptions = $getArchiveOptions->get(); 18 | $tags = $getTagOptions->get(); 19 | 20 | return view('blog::post-index', [ 21 | 'posts' => $posts, 22 | 'archiveOptions' => $archiveOptions, 23 | 'tags' => $tags, 24 | 'fromArchive' => $archiveDate, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/Blog/Http/Controllers/SitePostController.php: -------------------------------------------------------------------------------- 1 | where('published_at', '<=', Carbon::now()) 18 | ->latest() 19 | ->paginate(6); 20 | 21 | $archiveOptions = $getArchiveOptions->get(); 22 | $tags = $getTagOptions->get(); 23 | 24 | return view('blog::post-index', compact('posts', 'archiveOptions', 'tags')); 25 | } 26 | 27 | public function show($slug) 28 | { 29 | $post = Post::with('author')->where('slug', $slug)->first(); 30 | 31 | return view('blog::post-show', compact('post')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/Blog/Http/Controllers/SitePostSearchController.php: -------------------------------------------------------------------------------- 1 | where('published_at', '<=', Carbon::now()) 18 | ->search('title,content', $searchTerm) 19 | ->latest() 20 | ->paginate(6); 21 | 22 | $archiveOptions = $getArchiveOptions->get(); 23 | $tags = $getTagOptions->get(); 24 | 25 | $fromSearch = $searchTerm; 26 | 27 | return view('blog::post-index', compact('posts', 'archiveOptions', 'tags', 'fromSearch')); 28 | } 29 | 30 | public function show($slug): View 31 | { 32 | $post = Post::where('slug', $slug)->first(); 33 | 34 | return view('blog::post-show', compact('post')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/Blog/Http/Controllers/SiteTagController.php: -------------------------------------------------------------------------------- 1 | whereHas('tags', function ($query) use ($tagSlug) { 19 | $query->where('slug', $tagSlug); 20 | }) 21 | ->where('published_at', '<=', Carbon::now()) 22 | ->latest() 23 | ->paginate(6); 24 | 25 | $archiveOptions = $getArchiveOptions->get(); 26 | $tag = Tag::where('slug', $tagSlug)->first(); 27 | 28 | $tags = $getTagOptions->get(); 29 | 30 | return view('blog::post-index', [ 31 | 'posts' => $posts, 32 | 'archiveOptions' => $archiveOptions, 33 | 'tags' => $tags, 34 | 'fromTag' => $tag ? $tag->name : null, 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/Blog/Http/Requests/AuthorValidate.php: -------------------------------------------------------------------------------- 1 | 'required|string|max:255', 14 | 'email' => [ 15 | 'required', 16 | 'string', 17 | 'email', 18 | 'max:255', 19 | Rule::unique('blog_authors', 'email')->ignore(request('id')), 20 | ], 21 | 'image' => 'nullable|image|max:2048', // Max size 2MB 22 | 'bio' => 'nullable|string', 23 | 'github_handle' => 'nullable|string|max:255', 24 | 'twitter_handle' => 'nullable|string|max:255', 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/Blog/Http/Requests/CategoryValidate.php: -------------------------------------------------------------------------------- 1 | 'required|string|max:255', 13 | 'description' => 'nullable|string', 14 | 'image' => 'nullable|image|max:2048', // Max size 2MB 15 | 'is_visible' => 'required|boolean', 16 | 'meta_tag_title' => 'nullable|string|max:60', 17 | 'meta_tag_description' => 'nullable|string|max:160', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/Blog/Http/Requests/PostValidate.php: -------------------------------------------------------------------------------- 1 | 'nullable|exists:blog_authors,id', 13 | 'blog_category_id' => 'nullable|exists:blog_categories,id', 14 | 'title' => 'required|string|max:255', 15 | 'content' => 'required|string', 16 | 'summary' => 'nullable|string|max:65535', // database text field size 17 | 'image' => 'nullable|image|max:2048', // Max size 2MB 18 | 'meta_tag_title' => 'nullable|string|max:60', 19 | 'meta_tag_description' => 'nullable|string|max:160', 20 | 'published_at' => 'nullable|date', 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/Blog/Http/Requests/TagValidate.php: -------------------------------------------------------------------------------- 1 | 'required|string|max:255', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/Blog/Models/Author.php: -------------------------------------------------------------------------------- 1 | image) { 26 | return asset("storage/blog/{$this->image}"); 27 | } 28 | 29 | return null; 30 | } 31 | 32 | protected static function newFactory(): Factory 33 | { 34 | return BlogAuthorFactory::new(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/Blog/Models/Category.php: -------------------------------------------------------------------------------- 1 | 'boolean', 26 | ]; 27 | 28 | public function sluggable(): array 29 | { 30 | return [ 31 | 'slug' => [ 32 | 'source' => 'name', 33 | ], 34 | ]; 35 | } 36 | 37 | public function getImageUrlAttribute(): ?string 38 | { 39 | if ($this->image) { 40 | return asset("storage/blog/{$this->image}"); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | protected static function newFactory(): Factory 47 | { 48 | return BlogCategoryFactory::new(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/Blog/Models/Post.php: -------------------------------------------------------------------------------- 1 | 'datetime:Y-m-d', 28 | ]; 29 | 30 | public function tags(): BelongsToMany 31 | { 32 | return $this->belongsToMany(Tag::class, 'blog_posts_tags', 'blog_post_id', 'blog_tag_id'); 33 | } 34 | 35 | public function sluggable(): array 36 | { 37 | return [ 38 | 'slug' => [ 39 | 'source' => 'title', 40 | ], 41 | ]; 42 | } 43 | 44 | public function getStatusAttribute(): string 45 | { 46 | if ($this->published_at and Carbon::now()->greaterThan($this->published_at)) { 47 | return 'Published'; 48 | } 49 | 50 | return 'Draft'; 51 | } 52 | 53 | public function getImageUrlAttribute(): ?string 54 | { 55 | if ($this->image) { 56 | return asset("storage/blog/{$this->image}"); 57 | } 58 | 59 | return null; 60 | } 61 | 62 | protected static function newFactory(): Factory 63 | { 64 | return BlogPostFactory::new(); 65 | } 66 | 67 | public function author() 68 | { 69 | return $this->belongsTo(Author::class, 'blog_author_id'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /modules/Blog/Models/Tag.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Post::class, 'blog_posts_tags', 'blog_tag_id', 'blog_post_id'); 26 | } 27 | 28 | public function sluggable(): array 29 | { 30 | return [ 31 | 'slug' => [ 32 | 'source' => 'name', 33 | ], 34 | ]; 35 | } 36 | 37 | protected static function newFactory(): Factory 38 | { 39 | return BlogTagFactory::new(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /modules/Blog/Observers/PostObserver.php: -------------------------------------------------------------------------------- 1 | setMetaTagTitle($post); 13 | $this->setMetaTagDescription($post); 14 | } 15 | 16 | private function setMetaTagTitle(Post $post): void 17 | { 18 | if (! request()->has('meta_tag_title') or empty(request('meta_tag_title'))) { 19 | $post->meta_tag_title = Str::limit($post->title, 60, ''); 20 | } 21 | } 22 | 23 | private function setMetaTagDescription(Post $post): void 24 | { 25 | if (! request()->has('meta_tag_description') or empty(request('meta_tag_description'))) { 26 | $description = strip_tags((string) $post->content); 27 | 28 | // Add a space after punctuation, with exceptions for digits and ellipsis 29 | $pattern = [ 30 | '/(\.\.\.)(?=[^\s])/u', // Match ellipsis not followed by a space 31 | '/(?meta_tag_description = Str::limit($description, 160, ''); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /modules/Blog/Services/GetAuthorOptions.php: -------------------------------------------------------------------------------- 1 | get() 14 | ->map(fn ($author) => [ 15 | 'value' => $author->id, 16 | 'label' => Str::limit($author->name, 25), 17 | ]) 18 | ->all(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/Blog/Services/GetCategoryOptions.php: -------------------------------------------------------------------------------- 1 | get() 14 | ->map(fn ($category) => [ 15 | 'value' => $category->id, 16 | 'label' => Str::limit($category->name, 25), 17 | ]) 18 | ->all(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/Blog/Services/GetTagOptions.php: -------------------------------------------------------------------------------- 1 | get() 14 | ->map(fn ($tag) => [ 15 | 'value' => $tag->id, 16 | 'label' => Str::limit($tag->name, 25), 17 | ]) 18 | ->all(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/Blog/Services/Site/GetArchiveOptions.php: -------------------------------------------------------------------------------- 1 | where('published_at', '<=', Carbon::now()) 14 | ->get(['published_at']); 15 | 16 | if ($posts->isEmpty()) { 17 | return [['value' => '', 'label' => 'No posts found']]; 18 | } 19 | 20 | $archiveDates = $posts->map(function ($post) { 21 | $date = Carbon::parse($post->published_at); 22 | $monthYear = $date->format('m-Y'); 23 | $label = $date->format('F').' ('.$date->format('Y').')'; 24 | 25 | return ['value' => $monthYear, 'label' => $label]; 26 | }); 27 | 28 | // Remove duplicates based on the 'value' property 29 | $uniqueArchiveDates = $archiveDates->unique('value')->values(); 30 | 31 | // Sort by date descending 32 | $sortedArchiveDates = $uniqueArchiveDates->sortByDesc(function ($date) { 33 | return Carbon::createFromFormat('m-Y', $date['value']); 34 | })->values(); 35 | 36 | return $sortedArchiveDates->toArray(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/Blog/Services/Site/GetPostsFromArchive.php: -------------------------------------------------------------------------------- 1 | startOfMonth()->format('Y-m-d H:i:s'); 15 | $endOfMonth = $archiveDateCarbon->endOfMonth()->format('Y-m-d H:i:s'); 16 | 17 | $posts = Post::with('tags') 18 | ->whereBetween('published_at', [$startOfMonth, $endOfMonth]) 19 | ->latest() 20 | ->paginate(6); 21 | 22 | return $posts; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/Blog/Services/Site/GetTagOptions.php: -------------------------------------------------------------------------------- 1 | where('published_at', '<=', Carbon::now()); 14 | }) 15 | ->orderBy('name') 16 | ->get(['name', 'slug']); 17 | 18 | if ($tags->isEmpty()) { 19 | return [ 20 | ['name' => 'No tags found', 'slug' => ''], 21 | ]; 22 | } 23 | 24 | return $tags->toArray(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/Blog/Services/SyncPostTags.php: -------------------------------------------------------------------------------- 1 | tags()->detach(); 13 | 14 | return; 15 | } 16 | 17 | $data = []; 18 | foreach ($tags as $tag) { 19 | $data[] = [ 20 | 'blog_post_id' => $post->id, 21 | 'blog_tag_id' => $tag['id'], 22 | ]; 23 | } 24 | $post->tags()->sync($data); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Services/GetAuthorOptionsTest.php: -------------------------------------------------------------------------------- 1 | create(['name' => 'Alanis', 'email' => 'test22@test.com']); 14 | $author2 = Author::factory()->create(['name' => 'Beatrice', 'email' => 'test23@test.com']); 15 | $getAuthorOptions = new GetAuthorOptions; 16 | 17 | // Act 18 | $options = $getAuthorOptions->get(); 19 | 20 | // Assert 21 | expect($options)->toBeArray(); 22 | expect($options)->toHaveCount(2); 23 | expect($options[0])->toHaveKeys(['value', 'label']); 24 | expect($options[0]['value'])->toBe($author1->id); 25 | expect($options[0]['label'])->toBe(Str::limit($author1->name, 25)); 26 | expect($options[1])->toHaveKeys(['value', 'label']); 27 | expect($options[1]['value'])->toBe($author2->id); 28 | expect($options[1]['label'])->toBe(Str::limit($author2->name, 25)); 29 | }); 30 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Services/GetCategoryOptionsTest.php: -------------------------------------------------------------------------------- 1 | create(['name' => 'Category 1']); 14 | $category2 = Category::factory()->create(['name' => 'Category 2']); 15 | $getCategoryOptions = new GetCategoryOptions; 16 | 17 | // Act 18 | $options = $getCategoryOptions->get(); 19 | 20 | // Assert 21 | expect($options)->toBeArray(); 22 | expect($options)->toHaveCount(2); 23 | expect($options[0])->toHaveKeys(['value', 'label']); 24 | expect($options[0]['value'])->toBe($category1->id); 25 | expect($options[0]['label'])->toBe(Str::limit($category1->name, 25)); 26 | expect($options[1])->toHaveKeys(['value', 'label']); 27 | expect($options[1]['value'])->toBe($category2->id); 28 | expect($options[1]['label'])->toBe(Str::limit($category2->name, 25)); 29 | }); 30 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Services/GetTagOptionsTest.php: -------------------------------------------------------------------------------- 1 | create(['name' => 'Tag 1']); 14 | $tag2 = Tag::factory()->create(['name' => 'Tag 2']); 15 | $tag3 = Tag::factory()->create(['name' => 'Tag 3']); 16 | 17 | // Instantiate the GetTagOptions service 18 | $service = new GetTagOptions; 19 | 20 | // Call the get method 21 | $options = $service->get(); 22 | 23 | // Assert that the options are correct 24 | $this->assertCount(3, $options); 25 | 26 | $this->assertEquals($tag1->id, $options[0]['value']); 27 | $this->assertEquals(Str::limit($tag1->name, 25), $options[0]['label']); 28 | 29 | $this->assertEquals($tag2->id, $options[1]['value']); 30 | $this->assertEquals(Str::limit($tag2->name, 25), $options[1]['label']); 31 | 32 | $this->assertEquals($tag3->id, $options[2]['value']); 33 | $this->assertEquals(Str::limit($tag3->name, 25), $options[2]['label']); 34 | }); 35 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Services/SyncPostTagsTest.php: -------------------------------------------------------------------------------- 1 | create(); 14 | $tag1 = Tag::factory()->create(); 15 | $tag2 = Tag::factory()->create(); 16 | $tags = [ 17 | ['id' => $tag1->id], 18 | ['id' => $tag2->id], 19 | ]; 20 | 21 | // Act 22 | (new SyncPostTags)->sync($post, $tags); 23 | 24 | // Assert 25 | $this->assertCount(2, $post->tags); 26 | $this->assertTrue($post->tags->contains($tag1)); 27 | $this->assertTrue($post->tags->contains($tag2)); 28 | }); 29 | 30 | test('sync method detaches and syncs tags with the post', function () { 31 | // Arrange 32 | $post = Post::factory()->create(); 33 | $tag1 = Tag::factory()->create(); 34 | $tag2 = Tag::factory()->create(); 35 | $tag3 = Tag::factory()->create(); 36 | $post->tags()->attach([$tag1->id, $tag2->id]); 37 | 38 | $tags = [ 39 | ['id' => $tag2->id], 40 | ['id' => $tag3->id], 41 | ]; 42 | 43 | // Act 44 | (new SyncPostTags)->sync($post, $tags); 45 | 46 | // Assert 47 | $this->assertCount(2, $post->tags); 48 | $this->assertFalse($post->tags->contains($tag1)); 49 | $this->assertTrue($post->tags->contains($tag2)); 50 | $this->assertTrue($post->tags->contains($tag3)); 51 | }); 52 | 53 | test('sync method detaches all tags when no tags are provided', function () { 54 | // Arrange 55 | $post = Post::factory()->create(); 56 | $tag1 = Tag::factory()->create(); 57 | $tags = [ 58 | ['id' => $tag1->id], 59 | ]; 60 | (new SyncPostTags)->sync($post, $tags); 61 | 62 | // Act 63 | (new SyncPostTags)->sync($post, []); 64 | 65 | // Assert 66 | $this->assertCount(0, $post->tags); 67 | }); 68 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Site/PostTest.php: -------------------------------------------------------------------------------- 1 | post = Post::factory()->create([ 12 | 'title' => 'Test Post', 13 | 'content' => 'Test Content', 14 | 'published_at' => now()->subDay(), 15 | ]); 16 | }); 17 | 18 | afterEach(function () { 19 | if ($this->post->image) { 20 | Storage::disk('public')->delete('blog/'.$this->post->image); 21 | } 22 | }); 23 | 24 | test('blog index page can be rendered', function () { 25 | $this->withoutVite(); 26 | $response = $this->get('/blog'); 27 | 28 | $response->assertStatus(200); 29 | $response->assertSee('Blog'); 30 | $response->assertSee($this->post->title); 31 | }); 32 | 33 | test('blog post page can be rendered', function () { 34 | $this->withoutVite(); 35 | $response = $this->get('/blog/'.$this->post->slug); 36 | 37 | $response->assertStatus(200); 38 | $response->assertSee($this->post->title); 39 | $response->assertSee($this->post->content); 40 | }); 41 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Site/Services/GetArchiveOptionsTest.php: -------------------------------------------------------------------------------- 1 | create(['published_at' => '2022-02-01']); 13 | Post::factory()->create(['published_at' => '2022-01-01']); 14 | Post::factory()->create(['published_at' => '2021-12-01']); 15 | 16 | $expectedOptions = [ 17 | ['value' => '02-2022', 'label' => 'February (2022)'], 18 | ['value' => '01-2022', 'label' => 'January (2022)'], 19 | ['value' => '12-2021', 'label' => 'December (2021)'], 20 | ]; 21 | 22 | // Act 23 | $options = (new GetArchiveOptions)->get(); 24 | 25 | // Assert 26 | $this->assertCount(3, $options); 27 | $this->assertEquals($expectedOptions, $options); 28 | }); 29 | 30 | test('get method returns "No posts found" when no posts are available', function () { 31 | // Act 32 | $options = (new GetArchiveOptions)->get(); 33 | 34 | // Assert 35 | $this->assertCount(1, $options); 36 | $this->assertEquals('', $options[0]['value']); 37 | $this->assertEquals('No posts found', $options[0]['label']); 38 | }); 39 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Site/Services/GetPostsFromArchiveTest.php: -------------------------------------------------------------------------------- 1 | create(['published_at' => '2022-02-01']); 18 | Post::factory()->create(['published_at' => '2022-01-01']); 19 | Post::factory()->create(['published_at' => '2021-12-01']); 20 | 21 | $expectedPosts = Post::with('tags') 22 | ->whereBetween('published_at', [$startOfMonth, $endOfMonth]) 23 | ->latest() 24 | ->paginate(6); 25 | 26 | // Act 27 | $service = new GetPostsFromArchive; 28 | $posts = $service->get($archiveDate); 29 | 30 | // Assert 31 | $this->assertInstanceOf(LengthAwarePaginator::class, $posts); 32 | $this->assertEquals($expectedPosts->count(), $posts->count()); 33 | $this->assertEquals($expectedPosts->toArray(), $posts->toArray()); 34 | }); 35 | -------------------------------------------------------------------------------- /modules/Blog/Tests/Site/Services/GetTagOptionsTest.php: -------------------------------------------------------------------------------- 1 | create(); 13 | 14 | $tag->posts()->create([ 15 | 'title' => 'Test Post', 16 | 'content' => 'Test Content', 17 | 'published_at' => Carbon::now(), 18 | ]); 19 | 20 | // Call the get method 21 | $tagOptions = (new GetTagOptions)->get(); 22 | 23 | // Assert that the tag options array is not empty 24 | $this->assertNotEmpty($tagOptions); 25 | 26 | // Assert that the tag options array contains the tag name and slug 27 | $this->assertEquals($tag->name, $tagOptions[0]['name']); 28 | $this->assertEquals($tag->slug, $tagOptions[0]['slug']); 29 | }); 30 | 31 | test('it returns a default tag option when no tags are found', function () { 32 | // Create an instance of GetTagOptions 33 | $getTagOptions = new GetTagOptions; 34 | 35 | // Call the get method 36 | $tagOptions = $getTagOptions->get(); 37 | 38 | // Assert that the tag options array contains the default tag option 39 | $this->assertEquals('No tags found', $tagOptions[0]['name']); 40 | $this->assertEquals('', $tagOptions[0]['slug']); 41 | }); 42 | -------------------------------------------------------------------------------- /modules/Blog/routes/site.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/Dashboard/routes/app.php: -------------------------------------------------------------------------------- 1 | name('dashboard.index'); 9 | -------------------------------------------------------------------------------- /modules/Index/Http/Controllers/IndexController.php: -------------------------------------------------------------------------------- 1 | 'required', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/Index/IndexServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/views', 'index'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules/Index/Models/Index.php: -------------------------------------------------------------------------------- 1 | withoutVite(); 9 | $response = $this->get('/'); 10 | $response->assertStatus(200); 11 | }); 12 | -------------------------------------------------------------------------------- /modules/Index/routes/site.php: -------------------------------------------------------------------------------- 1 | name('index.index'); 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/Support/Http/Controllers/AppController.php: -------------------------------------------------------------------------------- 1 | middleware('auth.user'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/Support/Http/Controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | json([ 19 | 'errors' => $validator->errors(), 20 | ], 422)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/Support/Traits/ActivityLog.php: -------------------------------------------------------------------------------- 1 | logAll() 18 | ->logOnlyDirty() 19 | ->useLogName('system'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/Support/Validators/required_editor.php: -------------------------------------------------------------------------------- 1 |

') { 7 | return true; 8 | } 9 | 10 | return false; 11 | }, trans('modular::validation.required_editor')); 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/User/Database/Seeders/UserSeeder.php: -------------------------------------------------------------------------------- 1 | createTestUsers(); 16 | 17 | User::factory() 18 | ->count(12) 19 | ->create(); 20 | 21 | Schema::enableForeignKeyConstraints(); 22 | } 23 | 24 | private function createTestUsers(): void 25 | { 26 | $users = [ 27 | 'Root User' => 'root@ismodular.com', 28 | 'Content Author' => 'author@ismodular.com', 29 | 'Content Director' => 'director@ismodular.com', 30 | ]; 31 | 32 | foreach ($users as $userName => $userEmail) { 33 | User::factory()->create([ 34 | 'name' => $userName, 35 | 'email' => $userEmail, 36 | 'email_verified_at' => now(), 37 | 'password' => bcrypt('SuperSecretPassword123!@#'), 38 | ]); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "@inertiajs/vue3": "^2.0.0", 10 | "@tailwindcss/forms": "^0.5.6", 11 | "@tailwindcss/postcss": "^4.0.0", 12 | "@tailwindcss/vite": "^4.0.0", 13 | "@tiptap/extension-image": "^2.2.4", 14 | "@tiptap/extension-link": "^2.2.4", 15 | "@tiptap/extension-table": "^2.2.4", 16 | "@tiptap/extension-table-cell": "^2.2.4", 17 | "@tiptap/extension-table-header": "^2.2.4", 18 | "@tiptap/extension-table-row": "^2.2.4", 19 | "@tiptap/extension-underline": "^2.2.4", 20 | "@tiptap/extension-youtube": "^2.2.4", 21 | "@tiptap/starter-kit": "^2.2.4", 22 | "@tiptap/vue-3": "^2.2.4", 23 | "@vitejs/plugin-vue": "^5.0.4", 24 | "axios": "^1.8.2", 25 | "concurrently": "^9.0.1", 26 | "eslint": "^v9.5.0", 27 | "eslint-config-prettier": "^10.0.2", 28 | "eslint-plugin-vue": "^v9.32.0", 29 | "laravel-vite-plugin": "^1.2.0", 30 | "pinia": "^3.0.1", 31 | "postcss": "^8.4.35", 32 | "postcss-import": "^16.0.1", 33 | "prettier": "^3.4.2", 34 | "prettier-plugin-blade": "^2.1.18", 35 | "prettier-plugin-tailwindcss": "^v0.6.11", 36 | "remixicon": "^4.2.0", 37 | "tailwindcss": "^4.0.0", 38 | "unplugin-vue-components": "^v28.0.0", 39 | "vite": "^6.0.11", 40 | "vue": "^3.4.21" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources-site/css/blog.css: -------------------------------------------------------------------------------- 1 | .blog-post-content { 2 | @apply text-neutral-11 leading-relaxed; 3 | } 4 | 5 | .blog-post-content h1 { 6 | @apply mt-8 mb-4 text-3xl font-semibold; 7 | } 8 | 9 | .blog-post-content h2 { 10 | @apply mt-8 mb-4 text-2xl font-semibold; 11 | } 12 | 13 | .blog-post-content h3 { 14 | @apply mt-8 mb-4 text-xl font-semibold; 15 | } 16 | 17 | .blog-post-content p { 18 | @apply mb-6 text-base leading-relaxed tracking-normal; 19 | } 20 | 21 | .blog-post-content img { 22 | @apply mt-4 mb-8 rounded-lg shadow-lg; 23 | } 24 | 25 | .blog-post-content a { 26 | @apply text-primary-9 hover:underline; 27 | } 28 | -------------------------------------------------------------------------------- /resources-site/images/home-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources-site/images/home-img.png -------------------------------------------------------------------------------- /resources-site/js/Components/Blog/ArchiveSelector.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 42 | -------------------------------------------------------------------------------- /resources-site/js/Components/Blog/BlogToolbar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /resources-site/js/Components/Blog/SearchInput.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 40 | -------------------------------------------------------------------------------- /resources-site/js/Components/Blog/TagSelector.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 42 | -------------------------------------------------------------------------------- /resources-site/js/Components/IndexExampleComponent.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources-site/js/blog-app.js: -------------------------------------------------------------------------------- 1 | import { createVueApp } from './create-vue-app.js' 2 | import BlogToolbar from './Components/Blog/BlogToolbar.vue' 3 | 4 | createVueApp({ 5 | BlogToolbar 6 | }).mount('#app') 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/images/blog/034cb604-d3d0-448b-92f1-6a255fb4653d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/034cb604-d3d0-448b-92f1-6a255fb4653d.jpg -------------------------------------------------------------------------------- /resources/images/blog/0d2dc407-6b6f-4022-9565-1ecec79daf52.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/0d2dc407-6b6f-4022-9565-1ecec79daf52.jpg -------------------------------------------------------------------------------- /resources/images/blog/153cfa68-8ca8-4712-a1c1-1bd969796ef9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/153cfa68-8ca8-4712-a1c1-1bd969796ef9.jpg -------------------------------------------------------------------------------- /resources/images/blog/15dcfc3d-f8f4-470e-9477-72322e399731.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/15dcfc3d-f8f4-470e-9477-72322e399731.jpg -------------------------------------------------------------------------------- /resources/images/blog/2348a092-55d0-4b6d-b43d-3b34280dbed9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/2348a092-55d0-4b6d-b43d-3b34280dbed9.jpg -------------------------------------------------------------------------------- /resources/images/blog/24521518-b796-445f-8e9a-59233d8d78fa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/24521518-b796-445f-8e9a-59233d8d78fa.jpg -------------------------------------------------------------------------------- /resources/images/blog/2a7f2576-82a1-4f28-9f38-5a9cc216ba16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/2a7f2576-82a1-4f28-9f38-5a9cc216ba16.jpg -------------------------------------------------------------------------------- /resources/images/blog/3d7da554-1a6c-44f6-9f9f-2a6947446a65.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/3d7da554-1a6c-44f6-9f9f-2a6947446a65.jpg -------------------------------------------------------------------------------- /resources/images/blog/3fe0425a-96a4-49b5-bcde-adf75caa626d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/3fe0425a-96a4-49b5-bcde-adf75caa626d.jpg -------------------------------------------------------------------------------- /resources/images/blog/44a928b1-5148-4940-af48-2792f1151272.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/44a928b1-5148-4940-af48-2792f1151272.jpg -------------------------------------------------------------------------------- /resources/images/blog/463c0606-1382-49b3-8ba5-3a3b91c4ff09.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/463c0606-1382-49b3-8ba5-3a3b91c4ff09.jpg -------------------------------------------------------------------------------- /resources/images/blog/49814997-b619-4281-842f-8729f2ec151c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/49814997-b619-4281-842f-8729f2ec151c.jpg -------------------------------------------------------------------------------- /resources/images/blog/4c798d2c-c4a6-4293-b052-98a048914215.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/4c798d2c-c4a6-4293-b052-98a048914215.jpg -------------------------------------------------------------------------------- /resources/images/blog/4e8d6a91-e0ef-48aa-86cd-8d18699407c3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/4e8d6a91-e0ef-48aa-86cd-8d18699407c3.jpg -------------------------------------------------------------------------------- /resources/images/blog/500862ad-b078-4306-9348-39edf65f6d05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/500862ad-b078-4306-9348-39edf65f6d05.jpg -------------------------------------------------------------------------------- /resources/images/blog/548857a2-c1b5-490e-b3a2-20a28ba98e0f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/548857a2-c1b5-490e-b3a2-20a28ba98e0f.jpg -------------------------------------------------------------------------------- /resources/images/blog/57135dbe-7d31-4380-85cd-b78179ca2400.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/57135dbe-7d31-4380-85cd-b78179ca2400.jpg -------------------------------------------------------------------------------- /resources/images/blog/57b76f29-dd7d-4018-b172-a06e6ef4a4cf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/57b76f29-dd7d-4018-b172-a06e6ef4a4cf.jpg -------------------------------------------------------------------------------- /resources/images/blog/5a888786-090c-40f1-85e0-f86895311eeb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/5a888786-090c-40f1-85e0-f86895311eeb.jpg -------------------------------------------------------------------------------- /resources/images/blog/5f4ecece-6275-41b9-bd64-0e6f904a5df7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/5f4ecece-6275-41b9-bd64-0e6f904a5df7.jpg -------------------------------------------------------------------------------- /resources/images/blog/5febe62d-d9bd-4054-b5a1-83b796ca9d14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/5febe62d-d9bd-4054-b5a1-83b796ca9d14.jpg -------------------------------------------------------------------------------- /resources/images/blog/68255060-d85e-4081-a025-931789e6aa1d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/68255060-d85e-4081-a025-931789e6aa1d.jpg -------------------------------------------------------------------------------- /resources/images/blog/735aa22c-6aa0-4ced-9552-5494f0da4e5e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/735aa22c-6aa0-4ced-9552-5494f0da4e5e.jpg -------------------------------------------------------------------------------- /resources/images/blog/75fca485-2ea3-47d0-b609-9c8bd58d3438.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/75fca485-2ea3-47d0-b609-9c8bd58d3438.jpg -------------------------------------------------------------------------------- /resources/images/blog/77aa4a7a-de06-4ae6-8a1d-42322f532c53.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/77aa4a7a-de06-4ae6-8a1d-42322f532c53.jpg -------------------------------------------------------------------------------- /resources/images/blog/850e4903-66fe-4feb-a218-64058bb84a6a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/850e4903-66fe-4feb-a218-64058bb84a6a.jpg -------------------------------------------------------------------------------- /resources/images/blog/8c183760-1de3-4f6a-bef6-7241e7ebbb2f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/8c183760-1de3-4f6a-bef6-7241e7ebbb2f.jpg -------------------------------------------------------------------------------- /resources/images/blog/94ab2a3f-2300-4754-b458-2cc36cceac05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/94ab2a3f-2300-4754-b458-2cc36cceac05.jpg -------------------------------------------------------------------------------- /resources/images/blog/952536c8-e74c-4089-bce4-3cc3b474a2d2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/952536c8-e74c-4089-bce4-3cc3b474a2d2.jpg -------------------------------------------------------------------------------- /resources/images/blog/9ab99b32-1f04-495e-b5a2-5fa789dedd18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/9ab99b32-1f04-495e-b5a2-5fa789dedd18.jpg -------------------------------------------------------------------------------- /resources/images/blog/a98ea6d4-9f8b-4897-9c63-bdafc1acd602.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/a98ea6d4-9f8b-4897-9c63-bdafc1acd602.jpg -------------------------------------------------------------------------------- /resources/images/blog/b51306e7-f8f8-49b3-9083-e3fffccad1b4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/b51306e7-f8f8-49b3-9083-e3fffccad1b4.jpg -------------------------------------------------------------------------------- /resources/images/blog/b8cac8c5-c3d2-4a65-8ebb-1b74059fe309.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/b8cac8c5-c3d2-4a65-8ebb-1b74059fe309.jpg -------------------------------------------------------------------------------- /resources/images/blog/bac12f21-8eaa-437e-bf0c-2ae84ca2b02a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/bac12f21-8eaa-437e-bf0c-2ae84ca2b02a.jpg -------------------------------------------------------------------------------- /resources/images/blog/cd2dc303-24c8-41b3-b09e-83205a647d8f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/cd2dc303-24c8-41b3-b09e-83205a647d8f.jpg -------------------------------------------------------------------------------- /resources/images/blog/cf45d6f9-0ff0-41ae-966b-a7fa50f08e33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/cf45d6f9-0ff0-41ae-966b-a7fa50f08e33.jpg -------------------------------------------------------------------------------- /resources/images/blog/d27f5f87-f8dd-47df-8b2e-5086a5ba66c1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/d27f5f87-f8dd-47df-8b2e-5086a5ba66c1.jpg -------------------------------------------------------------------------------- /resources/images/blog/da4bc233-b12b-4548-904c-b3cf70711607.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/da4bc233-b12b-4548-904c-b3cf70711607.jpg -------------------------------------------------------------------------------- /resources/images/blog/dc44c3b1-6de3-4c47-80bb-e28de18f314b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/dc44c3b1-6de3-4c47-80bb-e28de18f314b.jpg -------------------------------------------------------------------------------- /resources/images/blog/e42eefc0-7f34-4645-ba21-d9c931daf08f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/e42eefc0-7f34-4645-ba21-d9c931daf08f.jpg -------------------------------------------------------------------------------- /resources/images/blog/e6ebdade-56ec-4d59-9da2-cf479b7d8c46.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/e6ebdade-56ec-4d59-9da2-cf479b7d8c46.jpg -------------------------------------------------------------------------------- /resources/images/blog/e9d82a31-b7a2-46d8-9da4-48f51f4207aa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/e9d82a31-b7a2-46d8-9da4-48f51f4207aa.jpg -------------------------------------------------------------------------------- /resources/images/blog/edb880ab-4e13-4594-b6d1-fa4125a00100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/edb880ab-4e13-4594-b6d1-fa4125a00100.jpg -------------------------------------------------------------------------------- /resources/images/blog/f0d5596c-d23a-4db7-97a5-9a298b924994.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/f0d5596c-d23a-4db7-97a5-9a298b924994.jpg -------------------------------------------------------------------------------- /resources/images/blog/f5e456c2-de4b-48c2-9554-54b8b8e7bfbc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/f5e456c2-de4b-48c2-9554-54b8b8e7bfbc.jpg -------------------------------------------------------------------------------- /resources/images/blog/f6d0c648-5d59-4b96-9c44-ebdc0ba3ee38.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/f6d0c648-5d59-4b96-9c44-ebdc0ba3ee38.jpg -------------------------------------------------------------------------------- /resources/images/blog/faa02464-6d3e-49b9-a45c-de303c91f3d5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/faa02464-6d3e-49b9-a45c-de303c91f3d5.jpg -------------------------------------------------------------------------------- /resources/images/blog/fe15a944-36e4-4c0b-a634-f9e6c00df101.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModularThink/modular-demo/622f878fe8affbc32037f8ebdd7c908840e3c945/resources/images/blog/fe15a944-36e4-4c0b-a634-f9e6c00df101.jpg -------------------------------------------------------------------------------- /resources/js/Components/Auth/AppAuthLogo.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/Components/Auth/AppAuthShell.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/Components/DataTable/AppDataSearch.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /resources/js/Components/DataTable/AppDataTable.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /resources/js/Components/DataTable/AppDataTableData.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/js/Components/DataTable/AppDataTableHead.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /resources/js/Components/DataTable/AppDataTableRow.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/Components/DataTable/AppPaginator.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 54 | -------------------------------------------------------------------------------- /resources/js/Components/Form/AppFormErrors.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /resources/js/Components/Form/AppInputDate.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /resources/js/Components/Form/AppInputPassword.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 48 | -------------------------------------------------------------------------------- /resources/js/Components/Form/AppInputText.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /resources/js/Components/Form/AppLabel.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/js/Components/Form/AppRadioButton.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /resources/js/Components/Form/AppTextArea.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | 50 | 76 | -------------------------------------------------------------------------------- /resources/js/Components/Form/TipTap/TipTapButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | -------------------------------------------------------------------------------- /resources/js/Components/Form/TipTap/TipTapDivider.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/Components/Menu/AppBreadCrumb.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /resources/js/Components/Menu/AppBreadCrumbItem.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /resources/js/Components/Menu/AppMenuItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /resources/js/Components/Menu/AppMenuSection.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /resources/js/Components/Message/AppAlert.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | 54 | -------------------------------------------------------------------------------- /resources/js/Components/Misc/AppButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 46 | -------------------------------------------------------------------------------- /resources/js/Components/Misc/AppCard.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /resources/js/Components/Misc/AppImageNotAvailable.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/js/Components/Misc/AppLink.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/Components/Misc/AppSectionHeader.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | -------------------------------------------------------------------------------- /resources/js/Components/Modules/Blog/AppImageNotAvailable.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/js/Layouts/GuestLayout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogCategory/Components/CategoryBody.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogCategory/Components/CategoryImage.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogCategory/Components/CategoryVisibility.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogPost/Components/PostAuthor.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogPost/Components/PostBody.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 45 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogPost/Components/PostCategory.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogPost/Components/PostImage.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogPost/Components/PostPublishDate.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /resources/js/Pages/BlogPost/Components/PostTags.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 77 | -------------------------------------------------------------------------------- /resources/js/Pages/Dashboard/Components/DashboardCard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 47 | 48 | 67 | -------------------------------------------------------------------------------- /resources/js/Pages/Dashboard/DashboardIndex.vue: -------------------------------------------------------------------------------- 1 | 45 | 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { createPinia } from 'pinia' 7 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' 8 | import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/index.js' 9 | 10 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel' 11 | 12 | // global components 13 | import { Link } from '@inertiajs/vue3' 14 | import Layout from './Layouts/AuthenticatedLayout.vue' 15 | 16 | import Translations from '@/Plugins/Translations' 17 | 18 | createInertiaApp({ 19 | title: (title) => `${title} - ${appName}`, 20 | resolve: (name) => { 21 | const page = resolvePageComponent( 22 | `./Pages/${name}.vue`, 23 | import.meta.glob('./Pages/**/*.vue') 24 | ) 25 | 26 | page.then((module) => { 27 | module.default.layout = module.default.layout || Layout 28 | }) 29 | 30 | return page 31 | }, 32 | setup({ el, App, props, plugin }) { 33 | return createApp({ render: () => h(App, props) }) 34 | .use(createPinia()) 35 | .use(plugin) 36 | .use(ZiggyVue, Ziggy) // eslint-disable-line no-undef 37 | .use(Translations) 38 | .component('Link', Link) 39 | .mount(el) 40 | }, 41 | progress: { 42 | color: '#3e63dd' 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/components/translations.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote'); 9 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 'Hello World!', 20 | // ]); 21 | // }); 22 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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: [ 14 | 'resources/js/app.js', 15 | 'resources-site/js/index-app.js', 16 | 'resources-site/css/site.css', 17 | 'resources-site/js/blog-app.js' 18 | ], 19 | refresh: [ 20 | 'resources/**/*', 21 | 'resources-site/**/*', 22 | 'modules/**/views/**/*.blade.php' 23 | ] 24 | }), 25 | vue({ 26 | template: { 27 | transformAssetUrls: { 28 | base: null, 29 | includeAbsolute: false 30 | } 31 | } 32 | }), 33 | Components({ 34 | resolvers: [AppComponentsResolver] 35 | }) 36 | ], 37 | resolve: { 38 | alias: { 39 | '@resources': '/resources', 40 | '@resourcesSite': '/resources-site' 41 | } 42 | } 43 | }) 44 | --------------------------------------------------------------------------------