├── .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 |
 }})
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 |
2 |
11 |
12 |
13 |
42 |
--------------------------------------------------------------------------------
/resources-site/js/Components/Blog/BlogToolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
29 |
--------------------------------------------------------------------------------
/resources-site/js/Components/Blog/SearchInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
40 |
--------------------------------------------------------------------------------
/resources-site/js/Components/Blog/TagSelector.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
42 |
--------------------------------------------------------------------------------
/resources-site/js/Components/IndexExampleComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | From ExampleComponent.vue: Let's do it!
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/resources/js/Components/Auth/AppAuthShell.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/resources/js/Components/DataTable/AppDataSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
51 |
--------------------------------------------------------------------------------
/resources/js/Components/DataTable/AppDataTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
--------------------------------------------------------------------------------
/resources/js/Components/DataTable/AppDataTableData.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | |
7 |
8 |
--------------------------------------------------------------------------------
/resources/js/Components/DataTable/AppDataTableHead.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 | {{ header }}
10 | |
11 |
12 |
13 |
14 |
15 |
23 |
--------------------------------------------------------------------------------
/resources/js/Components/DataTable/AppDataTableRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/resources/js/Components/DataTable/AppPaginator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Showing {{ from }} to {{ to }} of
5 | {{ total }} results
6 |
7 |
27 |
28 |
29 |
30 |
54 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/AppFormErrors.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ __('Whoops! Something went wrong...') }}
5 |
6 |
7 |
10 |
11 |
12 |
13 |
21 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/AppInputDate.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
16 |
17 |
39 |
40 |
45 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/AppInputPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
14 |
19 |
20 |
21 |
22 |
23 |
48 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/AppInputText.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
32 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/AppLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/AppRadioButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
45 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/AppTextArea.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
49 |
50 |
76 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/TipTap/TipTapButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
25 |
--------------------------------------------------------------------------------
/resources/js/Components/Form/TipTap/TipTapDivider.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/resources/js/Components/Menu/AppBreadCrumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
19 |
--------------------------------------------------------------------------------
/resources/js/Components/Menu/AppBreadCrumbItem.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ __(item.label) }}
3 |
4 |
5 | {{ __(item.label) }}
6 |
7 |
8 | >
9 |
10 |
11 |
19 |
--------------------------------------------------------------------------------
/resources/js/Components/Menu/AppMenuItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | {{ __(menuItem.label) }}
10 |
11 |
12 |
16 | {{ __(menuItem.label) }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
35 |
--------------------------------------------------------------------------------
/resources/js/Components/Menu/AppMenuSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
32 |
33 |
34 |
35 |
36 |
44 |
--------------------------------------------------------------------------------
/resources/js/Components/Message/AppAlert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
30 |
31 |
54 |
--------------------------------------------------------------------------------
/resources/js/Components/Misc/AppButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
46 |
--------------------------------------------------------------------------------
/resources/js/Components/Misc/AppCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/resources/js/Components/Misc/AppImageNotAvailable.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 | N/A
6 |
7 |
8 |
--------------------------------------------------------------------------------
/resources/js/Components/Misc/AppLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/resources/js/Components/Misc/AppSectionHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
31 |
--------------------------------------------------------------------------------
/resources/js/Components/Modules/Blog/AppImageNotAvailable.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 | N/A
6 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogCategory/Components/CategoryBody.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
Description
17 |
25 |
26 |
27 |
28 |
34 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogCategory/Components/CategoryImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
27 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogCategory/Components/CategoryVisibility.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
Turn Category visible
10 |
11 |
12 |
13 |
17 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogPost/Components/PostAuthor.vue:
--------------------------------------------------------------------------------
1 |
2 | Author
3 |
9 |
10 |
11 |
31 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogPost/Components/PostBody.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
25 |
26 |
37 |
38 |
39 |
45 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogPost/Components/PostCategory.vue:
--------------------------------------------------------------------------------
1 |
2 | Category
3 |
9 |
10 |
11 |
31 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogPost/Components/PostImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
27 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogPost/Components/PostPublishDate.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
17 |
--------------------------------------------------------------------------------
/resources/js/Pages/BlogPost/Components/PostTags.vue:
--------------------------------------------------------------------------------
1 |
2 | Tag
3 |
9 |
10 |
11 | -
16 |
17 | {{ tag.name }}
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
77 |
--------------------------------------------------------------------------------
/resources/js/Pages/Dashboard/Components/DashboardCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 | {{ label }}
12 |
13 |
14 | {{ count }}
15 |
16 |
17 |
18 |
19 |
20 |
47 |
48 |
67 |
--------------------------------------------------------------------------------
/resources/js/Pages/Dashboard/DashboardIndex.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Welcome
9 | {{ $page.props.auth.user.name }} !
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
33 |
34 |
35 |
43 |
44 |
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 |
--------------------------------------------------------------------------------