├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── app ├── Abstracts │ ├── MediaConversion.php │ └── ServiceForm.php ├── Actions │ ├── Fortify │ │ ├── CreateNewUser.php │ │ ├── PasswordValidationRules.php │ │ ├── ResetUserPassword.php │ │ ├── UpdateUserPassword.php │ │ └── UpdateUserProfileInformation.php │ ├── Jetstream │ │ ├── AddTeamMember.php │ │ ├── CreateTeam.php │ │ ├── DeleteTeam.php │ │ ├── DeleteUser.php │ │ ├── InviteTeamMember.php │ │ ├── RemoveTeamMember.php │ │ └── UpdateTeamName.php │ └── UpdateOrCreateService.php ├── Casts │ └── EncryptArrayObject.php ├── Contracts │ ├── Filter.php │ ├── MediaConversion.php │ └── ServiceForm.php ├── Facades │ ├── Services.php │ └── Settings.php ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── MediaController.php │ │ ├── MediaDownloadExternalController.php │ │ ├── MediaFetchGifsController.php │ │ ├── MediaFetchStockController.php │ │ ├── MediaFetchUploadsController.php │ │ ├── MediaUploadFileController.php │ │ ├── ServicesController.php │ │ └── SettingsController.php │ ├── Middleware │ │ └── HandleInertiaRequests.php │ ├── Requests │ │ ├── DeleteMedia.php │ │ ├── MediaDownloadExternal.php │ │ ├── MediaUploadFile.php │ │ ├── SaveService.php │ │ └── SaveSettings.php │ └── Resources │ │ └── MediaResource.php ├── Integrations │ └── Unsplash │ │ ├── Jobs │ │ └── TriggerDownloadJob.php │ │ └── Unsplash.php ├── MediaConversions │ ├── MediaImageResizeConversion.php │ └── MediaVideoThumbConversion.php ├── Models │ ├── Media.php │ ├── Membership.php │ ├── Service.php │ ├── Setting.php │ ├── Team.php │ ├── TeamInvitation.php │ └── User.php ├── Policies │ └── TeamPolicy.php ├── Providers │ ├── AppServiceProvider.php │ ├── BunnyServiceProvider.php │ ├── FortifyServiceProvider.php │ └── JetstreamServiceProvider.php ├── ServiceForm │ ├── BunnyStorageServiceForm.php │ ├── TenorServiceForm.php │ └── UnsplashServiceForm.php ├── Services.php ├── Settings.php ├── Support │ ├── File.php │ ├── Log.php │ ├── MediaConversionData.php │ ├── MediaFilesystem.php │ ├── MediaTemporaryDirectory.php │ ├── MediaUploader.php │ └── TimezoneList.php ├── Util.php └── helpers.php ├── artisan ├── auto-imports.d.ts ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── components.d.ts ├── components.json ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── filesystems.php ├── fortify.php ├── jetstream.php ├── logging.php ├── mail.php ├── mixpost.php ├── queue.php ├── sanctum.php ├── services.php └── session.php ├── database ├── .gitignore ├── factories │ ├── MediaFactory.php │ ├── TeamFactory.php │ └── 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 │ ├── 2024_11_12_150044_add_two_factor_columns_to_users_table.php │ ├── 2024_11_12_150107_create_personal_access_tokens_table.php │ ├── 2024_11_12_150108_create_teams_table.php │ ├── 2024_11_12_150109_create_team_user_table.php │ ├── 2024_11_12_150110_create_team_invitations_table.php │ ├── 2024_11_15_234958_create_media_table.php │ ├── 2024_11_15_235035_create_settings_table.php │ └── 2024_11_16_004034_create_services_table.php └── seeders │ └── DatabaseSeeder.php ├── env.d.ts ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .htaccess ├── assets │ ├── css │ │ └── loader.css │ └── doc │ │ └── services │ │ └── media │ │ └── tenor │ │ ├── gifs-library.png │ │ ├── tenor-api-key-created.png │ │ ├── tenor-create-api-key.png │ │ ├── tenor-form.png │ │ ├── tenor-service-1.png │ │ └── tenor-service-2.png ├── favicon.ico ├── index.php └── robots.txt ├── resources ├── assets │ └── images │ │ ├── auth-v2-login-illustration-bordered-dark.png │ │ ├── auth-v2-reset-password-illustration-dark.png │ │ ├── avatars │ │ ├── avatar-1.png │ │ ├── avatar-10.png │ │ ├── avatar-11.png │ │ ├── avatar-12.png │ │ ├── avatar-13.png │ │ ├── avatar-14.png │ │ ├── avatar-15.png │ │ ├── avatar-2.png │ │ ├── avatar-3.png │ │ ├── avatar-4.png │ │ ├── avatar-5.png │ │ ├── avatar-6.png │ │ ├── avatar-7.png │ │ ├── avatar-8.png │ │ └── avatar-9.png │ │ └── placeholder.svg ├── css │ └── app.css ├── markdown │ ├── policy.md │ ├── tenorDocumentation.md │ └── terms.md ├── ts │ ├── Pages │ │ ├── API │ │ │ ├── Index.vue │ │ │ └── Partials │ │ │ │ └── ApiTokenManager.vue │ │ ├── Auth │ │ │ ├── ConfirmPassword.vue │ │ │ ├── ForgotPassword.vue │ │ │ ├── Login.vue │ │ │ ├── Register.vue │ │ │ ├── ResetPassword.vue │ │ │ ├── TwoFactorChallenge.vue │ │ │ └── VerifyEmail.vue │ │ ├── Courses │ │ │ └── create.vue │ │ ├── Dashboard.vue │ │ ├── Error.vue │ │ ├── Media.vue │ │ ├── PrivacyPolicy.vue │ │ ├── Profile │ │ │ ├── Partials │ │ │ │ ├── DeleteUserForm.vue │ │ │ │ ├── LogoutOtherBrowserSessionsForm.vue │ │ │ │ ├── NavbarLink.vue │ │ │ │ ├── TwoFactorAuthenticationForm.vue │ │ │ │ ├── UpdatePasswordForm.vue │ │ │ │ └── UpdateProfileInformationForm.vue │ │ │ └── Show.vue │ │ ├── Services.vue │ │ ├── Teams │ │ │ ├── Create.vue │ │ │ ├── Partials │ │ │ │ ├── CreateTeamForm.vue │ │ │ │ ├── DeleteTeamForm.vue │ │ │ │ ├── TeamMemberManager.vue │ │ │ │ └── UpdateTeamNameForm.vue │ │ │ └── Show.vue │ │ ├── TermsOfService.vue │ │ └── Welcome.vue │ ├── Services │ │ └── emitter.ts │ ├── components │ │ ├── ActionMessage.vue │ │ ├── ActionSection.vue │ │ ├── ApplicationLogo.vue │ │ ├── Banner.vue │ │ ├── Checkbox.vue │ │ ├── ConfirmsPassword.vue │ │ ├── FormSection.vue │ │ ├── Icon.vue │ │ ├── InputError.vue │ │ ├── Masonry.vue │ │ ├── Media │ │ │ ├── AddMedia.vue │ │ │ ├── MediaCard.vue │ │ │ ├── MediaCredit.vue │ │ │ ├── MediaFile.vue │ │ │ ├── MediaGifs.vue │ │ │ ├── MediaSelectable.vue │ │ │ ├── MediaStock.vue │ │ │ ├── MediaUploads.vue │ │ │ └── UploadMedia.vue │ │ ├── Modal.vue │ │ ├── ModeToggle.vue │ │ ├── Notifications.vue │ │ ├── Preloader.vue │ │ ├── ReadDocHelp.vue │ │ ├── SectionBorder.vue │ │ ├── SelectableBar.vue │ │ ├── ServiceForm │ │ │ ├── BunnyStorageServiceForm.vue │ │ │ ├── TenorServiceForm.vue │ │ │ └── UnsplashServiceForm.vue │ │ └── Welcome.vue │ ├── composables │ │ ├── useApi.ts │ │ ├── useMedia.ts │ │ └── useNotifications.ts │ ├── core │ │ ├── components │ │ │ ├── alert-dialog │ │ │ │ ├── AlertDialog.vue │ │ │ │ ├── AlertDialogAction.vue │ │ │ │ ├── AlertDialogCancel.vue │ │ │ │ ├── AlertDialogContent.vue │ │ │ │ ├── AlertDialogDescription.vue │ │ │ │ ├── AlertDialogFooter.vue │ │ │ │ ├── AlertDialogHeader.vue │ │ │ │ ├── AlertDialogTitle.vue │ │ │ │ ├── AlertDialogTrigger.vue │ │ │ │ └── index.ts │ │ │ ├── alert │ │ │ │ ├── Alert.vue │ │ │ │ ├── AlertDescription.vue │ │ │ │ ├── AlertTitle.vue │ │ │ │ └── index.ts │ │ │ ├── avatar │ │ │ │ ├── Avatar.vue │ │ │ │ ├── AvatarFallback.vue │ │ │ │ ├── AvatarImage.vue │ │ │ │ └── index.ts │ │ │ ├── breadcrumb │ │ │ │ ├── Breadcrumb.vue │ │ │ │ ├── BreadcrumbEllipsis.vue │ │ │ │ ├── BreadcrumbItem.vue │ │ │ │ ├── BreadcrumbLink.vue │ │ │ │ ├── BreadcrumbList.vue │ │ │ │ ├── BreadcrumbPage.vue │ │ │ │ ├── BreadcrumbSeparator.vue │ │ │ │ └── index.ts │ │ │ ├── button │ │ │ │ ├── Button.vue │ │ │ │ └── index.ts │ │ │ ├── card │ │ │ │ ├── Card.vue │ │ │ │ ├── CardContent.vue │ │ │ │ ├── CardDescription.vue │ │ │ │ ├── CardFooter.vue │ │ │ │ ├── CardHeader.vue │ │ │ │ ├── CardTitle.vue │ │ │ │ └── index.ts │ │ │ ├── collapsible │ │ │ │ ├── Collapsible.vue │ │ │ │ ├── CollapsibleContent.vue │ │ │ │ ├── CollapsibleTrigger.vue │ │ │ │ └── index.ts │ │ │ ├── context-menu │ │ │ │ ├── ContextMenu.vue │ │ │ │ ├── ContextMenuCheckboxItem.vue │ │ │ │ ├── ContextMenuContent.vue │ │ │ │ ├── ContextMenuGroup.vue │ │ │ │ ├── ContextMenuItem.vue │ │ │ │ ├── ContextMenuLabel.vue │ │ │ │ ├── ContextMenuPortal.vue │ │ │ │ ├── ContextMenuRadioGroup.vue │ │ │ │ ├── ContextMenuRadioItem.vue │ │ │ │ ├── ContextMenuSeparator.vue │ │ │ │ ├── ContextMenuShortcut.vue │ │ │ │ ├── ContextMenuSub.vue │ │ │ │ ├── ContextMenuSubContent.vue │ │ │ │ ├── ContextMenuSubTrigger.vue │ │ │ │ ├── ContextMenuTrigger.vue │ │ │ │ └── index.ts │ │ │ ├── dialog │ │ │ │ ├── Dialog.vue │ │ │ │ ├── DialogClose.vue │ │ │ │ ├── DialogContent.vue │ │ │ │ ├── DialogDescription.vue │ │ │ │ ├── DialogFooter.vue │ │ │ │ ├── DialogHeader.vue │ │ │ │ ├── DialogScrollContent.vue │ │ │ │ ├── DialogTitle.vue │ │ │ │ ├── DialogTrigger.vue │ │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ │ ├── DropdownMenu.vue │ │ │ │ ├── DropdownMenuCheckboxItem.vue │ │ │ │ ├── DropdownMenuContent.vue │ │ │ │ ├── DropdownMenuGroup.vue │ │ │ │ ├── DropdownMenuItem.vue │ │ │ │ ├── DropdownMenuLabel.vue │ │ │ │ ├── DropdownMenuRadioGroup.vue │ │ │ │ ├── DropdownMenuRadioItem.vue │ │ │ │ ├── DropdownMenuSeparator.vue │ │ │ │ ├── DropdownMenuShortcut.vue │ │ │ │ ├── DropdownMenuSub.vue │ │ │ │ ├── DropdownMenuSubContent.vue │ │ │ │ ├── DropdownMenuSubTrigger.vue │ │ │ │ ├── DropdownMenuTrigger.vue │ │ │ │ └── index.ts │ │ │ ├── form │ │ │ │ ├── FormControl.vue │ │ │ │ ├── FormDescription.vue │ │ │ │ ├── FormItem.vue │ │ │ │ ├── FormLabel.vue │ │ │ │ ├── FormMessage.vue │ │ │ │ ├── index.ts │ │ │ │ ├── injectionKeys.ts │ │ │ │ └── useFormField.ts │ │ │ ├── input │ │ │ │ ├── Input.vue │ │ │ │ └── index.ts │ │ │ ├── label │ │ │ │ ├── Label.vue │ │ │ │ └── index.ts │ │ │ ├── pin-input │ │ │ │ ├── PinInput.vue │ │ │ │ ├── PinInputGroup.vue │ │ │ │ ├── PinInputInput.vue │ │ │ │ ├── PinInputSeparator.vue │ │ │ │ └── index.ts │ │ │ ├── separator │ │ │ │ ├── Separator.vue │ │ │ │ └── index.ts │ │ │ ├── sheet │ │ │ │ ├── Sheet.vue │ │ │ │ ├── SheetClose.vue │ │ │ │ ├── SheetContent.vue │ │ │ │ ├── SheetDescription.vue │ │ │ │ ├── SheetFooter.vue │ │ │ │ ├── SheetHeader.vue │ │ │ │ ├── SheetTitle.vue │ │ │ │ ├── SheetTrigger.vue │ │ │ │ └── index.ts │ │ │ ├── sidebar │ │ │ │ ├── Sidebar.vue │ │ │ │ ├── SidebarContent.vue │ │ │ │ ├── SidebarFooter.vue │ │ │ │ ├── SidebarGroup.vue │ │ │ │ ├── SidebarGroupAction.vue │ │ │ │ ├── SidebarGroupContent.vue │ │ │ │ ├── SidebarGroupLabel.vue │ │ │ │ ├── SidebarHeader.vue │ │ │ │ ├── SidebarInput.vue │ │ │ │ ├── SidebarInset.vue │ │ │ │ ├── SidebarMenu.vue │ │ │ │ ├── SidebarMenuAction.vue │ │ │ │ ├── SidebarMenuBadge.vue │ │ │ │ ├── SidebarMenuButton.vue │ │ │ │ ├── SidebarMenuButtonChild.vue │ │ │ │ ├── SidebarMenuItem.vue │ │ │ │ ├── SidebarMenuSkeleton.vue │ │ │ │ ├── SidebarMenuSub.vue │ │ │ │ ├── SidebarMenuSubButton.vue │ │ │ │ ├── SidebarMenuSubItem.vue │ │ │ │ ├── SidebarProvider.vue │ │ │ │ ├── SidebarRail.vue │ │ │ │ ├── SidebarSeparator.vue │ │ │ │ ├── SidebarTrigger.vue │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── skeleton │ │ │ │ ├── Skeleton.vue │ │ │ │ └── index.ts │ │ │ ├── sonner │ │ │ │ ├── Sonner.vue │ │ │ │ └── index.ts │ │ │ ├── tabs │ │ │ │ ├── Tabs.vue │ │ │ │ ├── TabsContent.vue │ │ │ │ ├── TabsList.vue │ │ │ │ ├── TabsTrigger.vue │ │ │ │ └── index.ts │ │ │ ├── toast │ │ │ │ ├── Toast.vue │ │ │ │ ├── ToastAction.vue │ │ │ │ ├── ToastClose.vue │ │ │ │ ├── ToastDescription.vue │ │ │ │ ├── ToastProvider.vue │ │ │ │ ├── ToastTitle.vue │ │ │ │ ├── ToastViewport.vue │ │ │ │ ├── Toaster.vue │ │ │ │ ├── index.ts │ │ │ │ └── use-toast.ts │ │ │ └── tooltip │ │ │ │ ├── Tooltip.vue │ │ │ │ ├── TooltipContent.vue │ │ │ │ ├── TooltipProvider.vue │ │ │ │ ├── TooltipTrigger.vue │ │ │ │ └── index.ts │ │ ├── composables │ │ │ ├── createUrl.ts │ │ │ ├── useCookie.ts │ │ │ └── useGenerateImageVariant.ts │ │ └── lib │ │ │ └── utils.ts │ ├── layouts │ │ ├── blank.vue │ │ ├── default.vue │ │ └── index.ts │ ├── lib │ │ ├── axios.ts │ │ ├── formatters.ts │ │ ├── helpers.ts │ │ ├── plugins.ts │ │ ├── sonner.ts │ │ └── validators.ts │ ├── main.ts │ ├── navigation │ │ └── index.ts │ ├── plugins │ │ ├── i18n │ │ │ ├── index.ts │ │ │ ├── locales │ │ │ │ ├── ar.json │ │ │ │ ├── en.json │ │ │ │ └── fr.json │ │ │ └── vue-i18n.d.ts │ │ └── iconify │ │ │ ├── build-icons.ts │ │ │ ├── icons.css │ │ │ ├── index.ts │ │ │ ├── package.json │ │ │ └── types.d.ts │ └── types.d.ts └── views │ ├── app.blade.php │ └── emails │ └── team-invitation.blade.php ├── routes ├── api.php ├── console.php └── web.php ├── shims.d.ts ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── ApiTokenPermissionsTest.php │ ├── AuthenticationTest.php │ ├── BrowserSessionsTest.php │ ├── CreateApiTokenTest.php │ ├── CreateTeamTest.php │ ├── DeleteAccountTest.php │ ├── DeleteApiTokenTest.php │ ├── DeleteTeamTest.php │ ├── EmailVerificationTest.php │ ├── ExampleTest.php │ ├── InviteTeamMemberTest.php │ ├── LeaveTeamTest.php │ ├── PasswordConfirmationTest.php │ ├── PasswordResetTest.php │ ├── ProfileInformationTest.php │ ├── RegistrationTest.php │ ├── RemoveTeamMemberTest.php │ ├── TwoFactorAuthenticationSettingsTest.php │ ├── UpdatePasswordTest.php │ ├── UpdateTeamMemberRoleTest.php │ └── UpdateTeamNameTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /bootstrap/ssr 3 | /node_modules 4 | /public/build 5 | /public/hot 6 | /public/storage 7 | /storage/*.key 8 | /storage/pail 9 | /vendor 10 | .env 11 | .env.backup 12 | .env.production 13 | .phpactor.json 14 | .phpunit.result.cache 15 | Homestead.json 16 | Homestead.yaml 17 | auth.json 18 | npm-debug.log 19 | yarn-error.log 20 | /.fleet 21 | /.idea 22 | /.vscode 23 | /.zed 24 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "useTabs": false, 6 | "trailingComma": "es5", 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "endOfLine": "lf", 10 | "printWidth": 150, 11 | "vueIndentScriptAndStyle": true 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 moikinge3@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/Abstracts/ServiceForm.php: -------------------------------------------------------------------------------- 1 | |string> 13 | */ 14 | protected function passwordRules(): array 15 | { 16 | return ['required', 'string', Password::default(), 'confirmed']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Actions/Fortify/ResetUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function reset(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'password' => $this->passwordRules(), 23 | ])->validate(); 24 | 25 | $user->forceFill([ 26 | 'password' => Hash::make($input['password']), 27 | ])->save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function update(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'current_password' => ['required', 'string', 'current_password:web'], 23 | 'password' => $this->passwordRules(), 24 | ], [ 25 | 'current_password.current_password' => __('The provided password does not match your current password.'), 26 | ])->validateWithBag('updatePassword'); 27 | 28 | $user->forceFill([ 29 | 'password' => Hash::make($input['password']), 30 | ])->save(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/CreateTeam.php: -------------------------------------------------------------------------------- 1 | $input 19 | */ 20 | public function create(User $user, array $input): Team 21 | { 22 | Gate::forUser($user)->authorize('create', Jetstream::newTeamModel()); 23 | 24 | Validator::make($input, [ 25 | 'name' => ['required', 'string', 'max:255'], 26 | ])->validateWithBag('createTeam'); 27 | 28 | AddingTeam::dispatch($user); 29 | 30 | $user->switchTeam($team = $user->ownedTeams()->create([ 31 | 'name' => $input['name'], 32 | 'personal_team' => false, 33 | ])); 34 | 35 | return $team; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/DeleteTeam.php: -------------------------------------------------------------------------------- 1 | purge(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/DeleteUser.php: -------------------------------------------------------------------------------- 1 | deleteTeams($user); 27 | $user->deleteProfilePhoto(); 28 | $user->tokens->each->delete(); 29 | $user->delete(); 30 | }); 31 | } 32 | 33 | /** 34 | * Delete the teams and team associations attached to the user. 35 | */ 36 | protected function deleteTeams(User $user): void 37 | { 38 | $user->teams()->detach(); 39 | 40 | $user->ownedTeams->each(function (Team $team) { 41 | $this->deletesTeams->delete($team); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/UpdateTeamName.php: -------------------------------------------------------------------------------- 1 | $input 17 | */ 18 | public function update(User $user, Team $team, array $input): void 19 | { 20 | Gate::forUser($user)->authorize('update', $team); 21 | 22 | Validator::make($input, [ 23 | 'name' => ['required', 'string', 'max:255'], 24 | ])->validateWithBag('updateTeamName'); 25 | 26 | $team->forceFill([ 27 | 'name' => $input['name'], 28 | ])->save(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Actions/UpdateOrCreateService.php: -------------------------------------------------------------------------------- 1 | $name], [ 13 | 'credentials' => $value, 14 | 'user_id' => $user?->id 15 | ]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Casts/EncryptArrayObject.php: -------------------------------------------------------------------------------- 1 | Crypt::encryptString(json_encode($value))]; 24 | } 25 | 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Contracts/Filter.php: -------------------------------------------------------------------------------- 1 | Services::isConfigured(), 18 | ]); 19 | } 20 | 21 | public function destroy(DeleteMedia $deleteMediaFiles): HttpResponse 22 | { 23 | $deleteMediaFiles->handle(); 24 | 25 | return response()->noContent(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/MediaDownloadExternalController.php: -------------------------------------------------------------------------------- 1 | handle(); 14 | 15 | return MediaResource::collection($media)->resolve(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Controllers/MediaFetchUploadsController.php: -------------------------------------------------------------------------------- 1 | where(function ($query) use ($request) { 17 | $query->where('name', 'like', "%{$request->query('keyword', '')}%") 18 | ->orWhere('mime_type', 'like', "%{$request->query('keyword', '')}%"); 19 | }) 20 | ->simplePaginate(30); 21 | 22 | return MediaResource::collection($records); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Controllers/MediaUploadFileController.php: -------------------------------------------------------------------------------- 1 | handle()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Http/Controllers/ServicesController.php: -------------------------------------------------------------------------------- 1 | Services::all() 21 | ]); 22 | } 23 | 24 | public function update(SaveService $saveService): RedirectResponse 25 | { 26 | $saveService->handle(); 27 | 28 | return back(); 29 | } 30 | 31 | /** 32 | * Show the terms of service for the application. 33 | * 34 | * @param \Illuminate\Http\Request $request 35 | * @return \Inertia\Response 36 | */ 37 | public function documentation(Request $request,$service) 38 | { 39 | $termsFile = Jetstream::localizedMarkdownPath('tenorDocumentation.md'); 40 | 41 | return Inertia::render('TermsOfService', [ 42 | 'terms' => Str::markdown(file_get_contents($termsFile)), 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/SettingsController.php: -------------------------------------------------------------------------------- 1 | Settings::all(), 19 | 'timezone_list' => (new TimezoneList())->splitGroup()->list(), 20 | ]); 21 | } 22 | 23 | public function update(SaveSettings $saveSettings): RedirectResponse 24 | { 25 | $saveSettings->handle(); 26 | 27 | return back(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Requests/DeleteMedia.php: -------------------------------------------------------------------------------- 1 | ['required', 'array'] 14 | ]; 15 | } 16 | 17 | public function handle() 18 | { 19 | foreach ($this->input('items') as $id) { 20 | $media = Media::find($id); 21 | 22 | if (!$media) { 23 | continue; 24 | } 25 | 26 | $media->deleteFiles(); 27 | $media->delete(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Requests/SaveService.php: -------------------------------------------------------------------------------- 1 | service()::rules(); 18 | 19 | return array_merge($serviceRules, [Rule::in($keys)]); 20 | } 21 | 22 | public function handle(): void 23 | { 24 | $form = $this->service()::form(); 25 | 26 | $value = Arr::map($form, function ($item, $key) { 27 | return $this->input($key); 28 | }); 29 | 30 | (new UpdateOrCreateService())($this->route('service'), $value, auth()->user()); 31 | } 32 | 33 | public function messages(): array 34 | { 35 | return $this->service()::messages(); 36 | } 37 | 38 | protected function service() 39 | { 40 | return ServicesFacade::services($this->route('service')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Http/Requests/SaveSettings.php: -------------------------------------------------------------------------------- 1 | $defaultPayload) { 21 | $payload = $this->input($name, $defaultPayload); 22 | 23 | SettingModel::updateOrCreate(['name' => $name], ['payload' => $payload]); 24 | 25 | SettingsFacade::put($name, $payload); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/MediaResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 15 | 'name' => $this->name, 16 | 'mime_type' => $this->mime_type, 17 | 'type' => $this->type(), 18 | 'url' => $this->getUrl(), 19 | 'thumb_url' => $this->isImageGif() ? $this->getUrl() : $this->getThumbUrl(), 20 | 'is_video' => $this->isVideo(), 21 | 'credit_url' => $this->credit_url ?? null, 22 | 'download_data' => $this->download_data ?? null, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Integrations/Unsplash/Jobs/TriggerDownloadJob.php: -------------------------------------------------------------------------------- 1 | downloadPhoto($this->downloadLocation); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Integrations/Unsplash/Unsplash.php: -------------------------------------------------------------------------------- 1 | clientId = $clientId; 24 | } 25 | 26 | public function photos(string $query = '', int $page = 1): array 27 | { 28 | return Http::get("$this->endpointUrl/search/photos", [ 29 | 'client_id' => $this->clientId, 30 | 'query' => $query ?: Arr::random(Util::config('external_media_terms')), 31 | 'page' => $page, 32 | 'per_page' => 30, 33 | ])->json('results', []); 34 | } 35 | 36 | public function downloadPhoto(string $downloadLocation) 37 | { 38 | $download_path = parse_url($downloadLocation, PHP_URL_PATH); 39 | $download_query = parse_url($downloadLocation, PHP_URL_QUERY); 40 | 41 | return Http::get("$this->endpointUrl$download_path?$download_query", [ 42 | 'client_id' => $this->clientId, 43 | ])->json(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Models/Membership.php: -------------------------------------------------------------------------------- 1 | EncryptArrayObject::class 22 | ]; 23 | 24 | protected $hidden = [ 25 | 'credentials' 26 | ]; 27 | 28 | public $timestamps = false; 29 | 30 | protected static function booted() 31 | { 32 | static::saved(function ($service) { 33 | ServicesFacade::put($service->name, $service->credentials->toArray()); 34 | }); 35 | 36 | static::deleted(function ($service) { 37 | ServicesFacade::forget($service->name); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Models/Setting.php: -------------------------------------------------------------------------------- 1 | 'array' 18 | ]; 19 | 20 | public $timestamps = false; 21 | 22 | protected static function booted() 23 | { 24 | static::saved(function ($setting) { 25 | SettingsFacade::put($setting->name, $setting->payload); 26 | }); 27 | 28 | static::deleted(function ($setting) { 29 | SettingsFacade::forget($setting->name); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Models/Team.php: -------------------------------------------------------------------------------- 1 | */ 14 | use HasFactory; 15 | 16 | /** 17 | * The attributes that are mass assignable. 18 | * 19 | * @var array 20 | */ 21 | protected $fillable = [ 22 | 'name', 23 | 'personal_team', 24 | ]; 25 | 26 | /** 27 | * The event map for the model. 28 | * 29 | * @var array 30 | */ 31 | protected $dispatchesEvents = [ 32 | 'created' => TeamCreated::class, 33 | 'updated' => TeamUpdated::class, 34 | 'deleted' => TeamDeleted::class, 35 | ]; 36 | 37 | /** 38 | * Get the attributes that should be cast. 39 | * 40 | * @return array 41 | */ 42 | protected function casts(): array 43 | { 44 | return [ 45 | 'personal_team' => 'boolean', 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Models/TeamInvitation.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $fillable = [ 17 | 'email', 18 | 'role', 19 | ]; 20 | 21 | /** 22 | * Get the team that the invitation belongs to. 23 | */ 24 | public function team(): BelongsTo 25 | { 26 | return $this->belongsTo(Jetstream::teamModel()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | '', 13 | 'api_key' => '', 14 | 'pull_zone' => '' 15 | ]; 16 | } 17 | 18 | public static function rules(): array 19 | { 20 | return [ 21 | "zone" => ['required', 'string'], 22 | "api_key" => ['required', 'string'], 23 | "pull_zone" => ['required', 'url'] 24 | ]; 25 | } 26 | 27 | public static function messages(): array 28 | { 29 | return [ 30 | // "zone" => 'The zone is required.', 31 | // "api_key" => 'The API Key is required.' 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/ServiceForm/TenorServiceForm.php: -------------------------------------------------------------------------------- 1 | 'https://tenor.com/gifapi', 14 | ]; 15 | static function form(): array 16 | { 17 | return [ 18 | 'client_id' => '' 19 | ]; 20 | } 21 | 22 | public static function rules(): array 23 | { 24 | return [ 25 | "client_id" => ['required'] 26 | ]; 27 | } 28 | 29 | public static function messages(): array 30 | { 31 | return [ 32 | 'client_id' => 'The API Key is required.' 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/ServiceForm/UnsplashServiceForm.php: -------------------------------------------------------------------------------- 1 | '' 13 | ]; 14 | } 15 | 16 | public static function rules(): array 17 | { 18 | return [ 19 | "client_id" => ['required'] 20 | ]; 21 | } 22 | 23 | public static function messages(): array 24 | { 25 | return [ 26 | 'client_id' => 'The API Key is required.' 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Support/File.php: -------------------------------------------------------------------------------- 1 | getPathname(), 27 | $tempFileObject->getFilename(), 28 | $tempFileObject->getMimeType(), 29 | 0, 30 | true // Mark it as test, since the file isn't from real HTTP POST. 31 | ); 32 | 33 | // Close this file after response is sent. 34 | // Closing the file will cause to remove it from temp director! 35 | app()->terminating(function () use ($tempFile) { 36 | fclose($tempFile); 37 | }); 38 | 39 | // return UploadedFile object 40 | return $file; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Support/Log.php: -------------------------------------------------------------------------------- 1 | info($message, $context); 12 | } 13 | 14 | static public function error(string $message, array $context = []): void 15 | { 16 | LogFacade::stack(self::stack())->error($message, $context); 17 | } 18 | 19 | static public function warning(string $message, array $context = []): void 20 | { 21 | LogFacade::stack(self::stack())->warning($message, $context); 22 | } 23 | 24 | static protected function stack(): array 25 | { 26 | if ($channel = config('mixpost.log_channel')) { 27 | return [$channel]; 28 | } 29 | 30 | return [config('app.log_channel')]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Support/MediaConversionData.php: -------------------------------------------------------------------------------- 1 | setConversion($conversion); 14 | } 15 | 16 | public static function conversion(MediaConversion $conversion): static 17 | { 18 | return new static($conversion); 19 | } 20 | 21 | public function get(): array 22 | { 23 | $reflection = new \ReflectionClass($this->conversion); 24 | 25 | return [ 26 | 'engine' => $this->conversion->getEngineName(), 27 | 'path' => $this->conversion->getPath(), 28 | 'disk' => $this->conversion->getToDisk(), 29 | 'name' => $reflection->getProperty('name')->getValue($this->conversion), 30 | ]; 31 | } 32 | 33 | private function setConversion(MediaConversion $conversion): static 34 | { 35 | $this->conversion = $conversion; 36 | 37 | return $this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Support/MediaFilesystem.php: -------------------------------------------------------------------------------- 1 | put($targetFile, File::get($sourceFile), 'public'); 18 | } 19 | 20 | protected static function getStream(string $filepath, string $disk) 21 | { 22 | return Storage::disk($disk)->readStream($filepath); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Support/MediaTemporaryDirectory.php: -------------------------------------------------------------------------------- 1 | flash('flash.banner', $message); 7 | session()->flash('flash.bannerStyle', $style); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.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 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/MediaFactory.php: -------------------------------------------------------------------------------- 1 | faker->randomDigit(); 15 | 16 | return [ 17 | 'name' => $this->faker->domainName, 18 | 'mime_type' => $this->faker->mimeType(), 19 | 'disk' => 'public', 20 | 'path' => '', 21 | 'size' => $size, 22 | 'size_total' => $size, 23 | 'conversions' => [] 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/factories/TeamFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class TeamFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition(): array 19 | { 20 | return [ 21 | 'name' => $this->faker->unique()->company(), 22 | 'user_id' => User::factory(), 23 | 'personal_team' => true, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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/2024_11_12_150107_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2024_11_12_150108_create_teams_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->index(); 17 | $table->string('name'); 18 | $table->boolean('personal_team'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('teams'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_11_12_150109_create_team_user_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('team_id'); 17 | $table->foreignId('user_id'); 18 | $table->string('role')->nullable(); 19 | $table->timestamps(); 20 | 21 | $table->unique(['team_id', 'user_id']); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('team_user'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2024_11_12_150110_create_team_invitations_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('team_id')->constrained()->cascadeOnDelete(); 17 | $table->string('email'); 18 | $table->string('role')->nullable(); 19 | $table->timestamps(); 20 | 21 | $table->unique(['team_id', 'email']); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('team_invitations'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2024_11_15_234958_create_media_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('mime_type'); 18 | $table->string('disk'); 19 | $table->string('path'); 20 | $table->unsignedBigInteger('size'); 21 | $table->unsignedBigInteger('size_total'); // including thumb file 22 | $table->json('conversions')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('media'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2024_11_15_235035_create_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete(); 17 | $table->string('name'); 18 | $table->json('payload'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('settings'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2024_11_16_004034_create_services_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete(); 17 | $table->string('name'); 18 | $table->longText('credentials'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('services'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | User::factory()->withPersonalTeam()->create([ 19 | 'name' => 'Test User', 20 | 'email' => 'moikinge3@gmail.com', 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | VITE_APP_NAME: string; 3 | VITE_GQL_URI: string; 4 | // others... 5 | } 6 | -------------------------------------------------------------------------------- /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.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/assets/doc/services/media/tenor/gifs-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/public/assets/doc/services/media/tenor/gifs-library.png -------------------------------------------------------------------------------- /public/assets/doc/services/media/tenor/tenor-api-key-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/public/assets/doc/services/media/tenor/tenor-api-key-created.png -------------------------------------------------------------------------------- /public/assets/doc/services/media/tenor/tenor-create-api-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/public/assets/doc/services/media/tenor/tenor-create-api-key.png -------------------------------------------------------------------------------- /public/assets/doc/services/media/tenor/tenor-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/public/assets/doc/services/media/tenor/tenor-form.png -------------------------------------------------------------------------------- /public/assets/doc/services/media/tenor/tenor-service-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/public/assets/doc/services/media/tenor/tenor-service-1.png -------------------------------------------------------------------------------- /public/assets/doc/services/media/tenor/tenor-service-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/public/assets/doc/services/media/tenor/tenor-service-2.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/assets/images/auth-v2-login-illustration-bordered-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/auth-v2-login-illustration-bordered-dark.png -------------------------------------------------------------------------------- /resources/assets/images/auth-v2-reset-password-illustration-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/auth-v2-reset-password-illustration-dark.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-1.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-10.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-11.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-12.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-13.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-14.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-15.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-2.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-3.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-4.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-5.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-6.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-7.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-8.png -------------------------------------------------------------------------------- /resources/assets/images/avatars/avatar-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugo-abdou/laravel-jetstream-shadcn-vue-starter/77decb23182be341a12dafb0fed7f2f1225e69a0/resources/assets/images/avatars/avatar-9.png -------------------------------------------------------------------------------- /resources/markdown/policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Edit this file to define the privacy policy for your application. 4 | -------------------------------------------------------------------------------- /resources/markdown/terms.md: -------------------------------------------------------------------------------- 1 | # Terms of Service 2 | 3 | Edit this file to define the terms of service for your application. 4 | -------------------------------------------------------------------------------- /resources/ts/Pages/API/Index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /resources/ts/Pages/Courses/create.vue: -------------------------------------------------------------------------------- 1 | 8 | 19 | -------------------------------------------------------------------------------- /resources/ts/Pages/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /resources/ts/Pages/Error.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 43 | -------------------------------------------------------------------------------- /resources/ts/Pages/PrivacyPolicy.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /resources/ts/Pages/Profile/Partials/NavbarLink.vue: -------------------------------------------------------------------------------- 1 | 6 | 15 | -------------------------------------------------------------------------------- /resources/ts/Pages/Teams/Create.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /resources/ts/Pages/Teams/Show.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /resources/ts/Pages/TermsOfService.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /resources/ts/Services/emitter.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | export default mitt(); 3 | -------------------------------------------------------------------------------- /resources/ts/components/ActionMessage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /resources/ts/components/ActionSection.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /resources/ts/components/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /resources/ts/components/FormSection.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /resources/ts/components/Icon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /resources/ts/components/InputError.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /resources/ts/components/Media/MediaCredit.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/ts/components/Media/MediaSelectable.vue: -------------------------------------------------------------------------------- 1 | 11 | 19 | -------------------------------------------------------------------------------- /resources/ts/components/ModeToggle.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | -------------------------------------------------------------------------------- /resources/ts/components/Notifications.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /resources/ts/components/ReadDocHelp.vue: -------------------------------------------------------------------------------- 1 | 11 | 17 | -------------------------------------------------------------------------------- /resources/ts/components/SectionBorder.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/ts/composables/useApi.ts: -------------------------------------------------------------------------------- 1 | import { createFetch } from '@vueuse/core'; 2 | import { destr } from 'destr'; 3 | import nProgress from 'nprogress'; 4 | 5 | export const useApi = createFetch({ 6 | baseUrl: import.meta.env.VITE_API_BASE_URL || '/api', 7 | fetchOptions: { 8 | headers: { 9 | Accept: 'application/json', 10 | }, 11 | }, 12 | options: { 13 | refetch: true, 14 | 15 | async beforeFetch({ options }) { 16 | nProgress.start() 17 | return { options }; 18 | }, 19 | afterFetch(ctx) { 20 | nProgress.done(); 21 | const { data, response } = ctx; 22 | 23 | // Parse data if it's JSON 24 | 25 | let parsedData = null; 26 | try { 27 | parsedData = destr(data); 28 | } catch (error) { 29 | console.error(error); 30 | } 31 | 32 | return { data: parsedData, response }; 33 | }, 34 | onFetchError(ctx) { 35 | nProgress.done(); 36 | return ctx; 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /resources/ts/composables/useNotifications.ts: -------------------------------------------------------------------------------- 1 | import emitter from '@/Services/emitter'; 2 | 3 | const useNotifications = () => { 4 | const notify = (variant: string, message: any, button?: any) => { 5 | if (typeof message !== 'object') { 6 | emitter.emit('notify', { variant, message, button }); 7 | } 8 | 9 | if (typeof message === 'object') { 10 | // Convert laravel validation errors to a string 11 | const text = Object.keys(message) 12 | .map((item) => message[item].join('\n')) 13 | .join('\n'); 14 | 15 | emitter.emit('notify', { variant, message: text, button }); 16 | } 17 | }; 18 | 19 | return { 20 | notify, 21 | }; 22 | }; 23 | 24 | export default useNotifications; 25 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialogAction.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialogCancel.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialogDescription.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialogFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialogTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/AlertDialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AlertDialog } from './AlertDialog.vue'; 2 | export { default as AlertDialogAction } from './AlertDialogAction.vue'; 3 | export { default as AlertDialogCancel } from './AlertDialogCancel.vue'; 4 | export { default as AlertDialogContent } from './AlertDialogContent.vue'; 5 | export { default as AlertDialogDescription } from './AlertDialogDescription.vue'; 6 | export { default as AlertDialogFooter } from './AlertDialogFooter.vue'; 7 | export { default as AlertDialogHeader } from './AlertDialogHeader.vue'; 8 | export { default as AlertDialogTitle } from './AlertDialogTitle.vue'; 9 | export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue'; 10 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert/Alert.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert/AlertDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/alert/AlertTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/avatar/Avatar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /resources/ts/core/components/avatar/AvatarFallback.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/avatar/AvatarImage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /resources/ts/core/components/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | 3 | export { default as Avatar } from './Avatar.vue'; 4 | export { default as AvatarFallback } from './AvatarFallback.vue'; 5 | export { default as AvatarImage } from './AvatarImage.vue'; 6 | 7 | export const avatarVariant = cva( 8 | 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', 9 | { 10 | variants: { 11 | size: { 12 | sm: 'h-10 w-10 text-xs', 13 | base: 'h-16 w-16 text-2xl', 14 | lg: 'h-32 w-32 text-5xl', 15 | }, 16 | shape: { 17 | circle: 'rounded-full', 18 | square: 'rounded-md', 19 | }, 20 | }, 21 | } 22 | ); 23 | 24 | export type AvatarVariants = VariantProps; 25 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/BreadcrumbEllipsis.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/BreadcrumbItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/BreadcrumbLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/BreadcrumbList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/BreadcrumbPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/BreadcrumbSeparator.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /resources/ts/core/components/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Breadcrumb } from './Breadcrumb.vue'; 2 | export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue'; 3 | export { default as BreadcrumbItem } from './BreadcrumbItem.vue'; 4 | export { default as BreadcrumbLink } from './BreadcrumbLink.vue'; 5 | export { default as BreadcrumbList } from './BreadcrumbList.vue'; 6 | export { default as BreadcrumbPage } from './BreadcrumbPage.vue'; 7 | export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue'; 8 | -------------------------------------------------------------------------------- /resources/ts/core/components/button/Button.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /resources/ts/core/components/card/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue'; 2 | export { default as CardContent } from './CardContent.vue'; 3 | export { default as CardDescription } from './CardDescription.vue'; 4 | export { default as CardFooter } from './CardFooter.vue'; 5 | export { default as CardHeader } from './CardHeader.vue'; 6 | export { default as CardTitle } from './CardTitle.vue'; 7 | -------------------------------------------------------------------------------- /resources/ts/core/components/collapsible/Collapsible.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /resources/ts/core/components/collapsible/CollapsibleContent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/collapsible/CollapsibleTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/collapsible/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Collapsible } from './Collapsible.vue'; 2 | export { default as CollapsibleContent } from './CollapsibleContent.vue'; 3 | export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'; 4 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuPortal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuRadioItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuSub.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuSubContent.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuSubTrigger.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 35 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/ContextMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /resources/ts/core/components/context-menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ContextMenu } from './ContextMenu.vue' 2 | export { default as ContextMenuCheckboxItem } from './ContextMenuCheckboxItem.vue' 3 | export { default as ContextMenuContent } from './ContextMenuContent.vue' 4 | export { default as ContextMenuGroup } from './ContextMenuGroup.vue' 5 | export { default as ContextMenuItem } from './ContextMenuItem.vue' 6 | export { default as ContextMenuLabel } from './ContextMenuLabel.vue' 7 | export { default as ContextMenuRadioGroup } from './ContextMenuRadioGroup.vue' 8 | export { default as ContextMenuRadioItem } from './ContextMenuRadioItem.vue' 9 | export { default as ContextMenuSeparator } from './ContextMenuSeparator.vue' 10 | export { default as ContextMenuShortcut } from './ContextMenuShortcut.vue' 11 | export { default as ContextMenuSub } from './ContextMenuSub.vue' 12 | export { default as ContextMenuSubContent } from './ContextMenuSubContent.vue' 13 | export { default as ContextMenuSubTrigger } from './ContextMenuSubTrigger.vue' 14 | export { default as ContextMenuTrigger } from './ContextMenuTrigger.vue' 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue'; 2 | export { default as DialogClose } from './DialogClose.vue'; 3 | export { default as DialogContent } from './DialogContent.vue'; 4 | export { default as DialogDescription } from './DialogDescription.vue'; 5 | export { default as DialogFooter } from './DialogFooter.vue'; 6 | export { default as DialogHeader } from './DialogHeader.vue'; 7 | export { default as DialogScrollContent } from './DialogScrollContent.vue'; 8 | export { default as DialogTitle } from './DialogTitle.vue'; 9 | export { default as DialogTrigger } from './DialogTrigger.vue'; 10 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuSub.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuSubTrigger.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/DropdownMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /resources/ts/core/components/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DropdownMenu } from './DropdownMenu.vue'; 2 | 3 | export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'; 4 | export { default as DropdownMenuContent } from './DropdownMenuContent.vue'; 5 | export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'; 6 | export { default as DropdownMenuItem } from './DropdownMenuItem.vue'; 7 | export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'; 8 | export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'; 9 | export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'; 10 | export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'; 11 | export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'; 12 | export { default as DropdownMenuSub } from './DropdownMenuSub.vue'; 13 | export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'; 14 | export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'; 15 | export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'; 16 | export { DropdownMenuPortal } from 'radix-vue'; 17 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/FormControl.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/FormDescription.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/FormItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/FormLabel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/FormMessage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FormControl } from './FormControl.vue'; 2 | export { default as FormDescription } from './FormDescription.vue'; 3 | export { default as FormItem } from './FormItem.vue'; 4 | export { default as FormLabel } from './FormLabel.vue'; 5 | export { default as FormMessage } from './FormMessage.vue'; 6 | export { FORM_ITEM_INJECTION_KEY } from './injectionKeys'; 7 | export { Field as FormField, Form } from 'vee-validate'; 8 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/injectionKeys.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue'; 2 | 3 | export const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey; 4 | -------------------------------------------------------------------------------- /resources/ts/core/components/form/useFormField.ts: -------------------------------------------------------------------------------- 1 | import { FieldContextKey, useFieldError, useIsFieldDirty, useIsFieldTouched, useIsFieldValid } from 'vee-validate'; 2 | import { inject } from 'vue'; 3 | import { FORM_ITEM_INJECTION_KEY } from './injectionKeys'; 4 | 5 | export function useFormField() { 6 | const fieldContext = inject(FieldContextKey); 7 | const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY); 8 | 9 | if (!fieldContext) throw new Error('useFormField should be used within '); 10 | 11 | const { name } = fieldContext; 12 | const id = fieldItemContext; 13 | 14 | const fieldState = { 15 | valid: useIsFieldValid(name), 16 | isDirty: useIsFieldDirty(name), 17 | isTouched: useIsFieldTouched(name), 18 | error: useFieldError(name), 19 | }; 20 | 21 | return { 22 | id, 23 | name, 24 | formItemId: `${id}-form-item`, 25 | formDescriptionId: `${id}-form-item-description`, 26 | formMessageId: `${id}-form-item-message`, 27 | ...fieldState, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /resources/ts/core/components/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /resources/ts/core/components/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue'; 2 | -------------------------------------------------------------------------------- /resources/ts/core/components/label/Label.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /resources/ts/core/components/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label.vue'; 2 | -------------------------------------------------------------------------------- /resources/ts/core/components/pin-input/PinInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /resources/ts/core/components/pin-input/PinInputGroup.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /resources/ts/core/components/pin-input/PinInputInput.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /resources/ts/core/components/pin-input/PinInputSeparator.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /resources/ts/core/components/pin-input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PinInput } from './PinInput.vue'; 2 | export { default as PinInputGroup } from './PinInputGroup.vue'; 3 | export { default as PinInputInput } from './PinInputInput.vue'; 4 | export { default as PinInputSeparator } from './PinInputSeparator.vue'; 5 | -------------------------------------------------------------------------------- /resources/ts/core/components/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | -------------------------------------------------------------------------------- /resources/ts/core/components/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue'; 2 | -------------------------------------------------------------------------------- /resources/ts/core/components/sheet/Sheet.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sheet/SheetClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/sheet/SheetDescription.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/sheet/SheetFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /resources/ts/core/components/sheet/SheetHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /resources/ts/core/components/sheet/SheetTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/sheet/SheetTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarGroupAction.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarGroupContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarGroupLabel.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarInput.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarInset.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarMenuBadge.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarMenuButtonChild.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarMenuItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarMenuSkeleton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarMenuSub.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarMenuSubItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarSeparator.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/SidebarTrigger.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /resources/ts/core/components/sidebar/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, Ref } from 'vue'; 2 | import { createContext } from 'radix-vue'; 3 | 4 | export const SIDEBAR_COOKIE_NAME = 'sidebar:state'; 5 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; 6 | export const SIDEBAR_WIDTH = '16rem'; 7 | export const SIDEBAR_WIDTH_MOBILE = '18rem'; 8 | export const SIDEBAR_WIDTH_ICON = '3rem'; 9 | export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; 10 | 11 | export const [useSidebar, provideSidebarContext] = createContext<{ 12 | state: ComputedRef<'expanded' | 'collapsed'>; 13 | open: Ref; 14 | setOpen: (value: boolean) => void; 15 | isMobile: Ref; 16 | openMobile: Ref; 17 | setOpenMobile: (value: boolean) => void; 18 | toggleSidebar: () => void; 19 | }>('Sidebar'); 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/skeleton/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Skeleton } from './Skeleton.vue'; 2 | -------------------------------------------------------------------------------- /resources/ts/core/components/sonner/Sonner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /resources/ts/core/components/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from './Sonner.vue' 2 | -------------------------------------------------------------------------------- /resources/ts/core/components/tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /resources/ts/core/components/tabs/TabsContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /resources/ts/core/components/tabs/TabsList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /resources/ts/core/components/tabs/TabsTrigger.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | -------------------------------------------------------------------------------- /resources/ts/core/components/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tabs } from './Tabs.vue'; 2 | export { default as TabsContent } from './TabsContent.vue'; 3 | export { default as TabsList } from './TabsList.vue'; 4 | export { default as TabsTrigger } from './TabsTrigger.vue'; 5 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/Toast.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/ToastAction.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/ToastClose.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/ToastDescription.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/ToastProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/ToastTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/ToastViewport.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /resources/ts/core/components/toast/Toaster.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | -------------------------------------------------------------------------------- /resources/ts/core/components/tooltip/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/ts/core/components/tooltip/TooltipProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/tooltip/TooltipTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/ts/core/components/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip.vue'; 2 | export { default as TooltipContent } from './TooltipContent.vue'; 3 | export { default as TooltipProvider } from './TooltipProvider.vue'; 4 | export { default as TooltipTrigger } from './TooltipTrigger.vue'; 5 | -------------------------------------------------------------------------------- /resources/ts/core/composables/createUrl.ts: -------------------------------------------------------------------------------- 1 | import { stringifyQuery } from 'ufo'; 2 | import type { MaybeRefOrGetter } from 'vue'; 3 | 4 | interface Options { 5 | query: MaybeRefOrGetter>; 6 | } 7 | 8 | export const createUrl = (url: MaybeRefOrGetter, options?: Options) => 9 | computed(() => { 10 | if (!options?.query) return toValue(url); 11 | 12 | const _url = toValue(url); 13 | const _query = toValue(options?.query); 14 | 15 | const queryObj = Object.fromEntries(Object.entries(_query).map(([key, val]) => [key, toValue(val)])); 16 | 17 | return `${_url}${queryObj ? `?${stringifyQuery(queryObj)}` : ''}`; 18 | }); 19 | -------------------------------------------------------------------------------- /resources/ts/core/composables/useGenerateImageVariant.ts: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'vuetify'; 2 | import { useConfigStore } from '@core/stores/config'; 3 | 4 | // composable function to return the image variant as per the current theme and skin 5 | export const useGenerateImageVariant = (imgLight: string, imgDark: string, imgLightBordered?: string, imgDarkBordered?: string, bordered = false) => { 6 | const configStore = useConfigStore(); 7 | const { global } = useTheme(); 8 | 9 | return computed(() => { 10 | if (global.name.value === 'light') { 11 | if (configStore.skin === 'bordered' && bordered) return imgLightBordered; 12 | else return imgLight; 13 | } 14 | if (global.name.value === 'dark') { 15 | if (configStore.skin === 'bordered' && bordered) return imgDarkBordered; 16 | else return imgDark; 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /resources/ts/core/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /resources/ts/layouts/blank.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /resources/ts/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { default as blank } from './blank.vue'; 2 | export { default as default } from './default.vue'; 3 | -------------------------------------------------------------------------------- /resources/ts/lib/axios.ts: -------------------------------------------------------------------------------- 1 | import _axios from 'axios'; 2 | // window.axios = axios; 3 | 4 | // window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 5 | 6 | const axios = _axios.create({ 7 | baseURL: window.location.origin, 8 | headers: { 9 | common: { 10 | 'X-Requested-With': 'XMLHttpRequest', 11 | }, 12 | }, 13 | }); 14 | axios.defaults.withCredentials = true; 15 | (window as any).axios = axios; 16 | export default axios; 17 | -------------------------------------------------------------------------------- /resources/ts/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | // 👉 IsEmpty 2 | export const isEmpty = (value: unknown): boolean => { 3 | if (value === null || value === undefined || value === '') return true; 4 | 5 | return !!(Array.isArray(value) && value.length === 0); 6 | }; 7 | 8 | // 👉 IsNullOrUndefined 9 | export const isNullOrUndefined = (value: unknown): value is undefined | null => { 10 | return value === null || value === undefined; 11 | }; 12 | 13 | // 👉 IsEmptyArray 14 | export const isEmptyArray = (arr: unknown): boolean => { 15 | return Array.isArray(arr) && arr.length === 0; 16 | }; 17 | 18 | // 👉 IsObject 19 | export const isObject = (obj: unknown): obj is Record => obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj); 20 | 21 | // 👉 IsToday 22 | export const isToday = (date: Date) => { 23 | const today = new Date(); 24 | 25 | return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); 26 | }; 27 | -------------------------------------------------------------------------------- /resources/ts/lib/sonner.ts: -------------------------------------------------------------------------------- 1 | import Icon from '@/components/Icon.vue'; 2 | import { ExternalToast,toast as sonnerToast } from 'vue-sonner'; 3 | 4 | 5 | export function sonnerInfo(message: string | Component, data?: ExternalToast) { 6 | sonnerToast(message, { 7 | icon: h(Icon, { name: 'tabler-info-octagon' }), 8 | class: 'border', 9 | closeButton: true, 10 | ...data, 11 | }); 12 | } 13 | 14 | export function sonnerSuccess(message: string | Component, data?: ExternalToast) { 15 | sonnerToast(message, { 16 | icon: h(Icon, { name: 'tabler-check' }), 17 | class: '!bg-success !text-success-foreground', 18 | closeButton: true, 19 | ...data, 20 | }); 21 | } 22 | 23 | export function sonnerError(message: string | Component, data?: ExternalToast) { 24 | sonnerToast(message, { 25 | icon: h(Icon, { name: 'tabler-x' }), 26 | class: '!bg-destructive !text-destructive-foreground', 27 | closeButton: true, 28 | ...data, 29 | }); 30 | } 31 | 32 | export function sonnerWarning(message: string | Component, data?: ExternalToast) { 33 | sonnerToast(message, { 34 | icon: h(Icon, { name: 'tabler-alert-triangle' }), 35 | class: '!bg-warning !text-warning-foreground', 36 | closeButton: true, 37 | ...data, 38 | }); 39 | } 40 | 41 | export { sonnerToast }; 42 | -------------------------------------------------------------------------------- /resources/ts/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, DefineComponent, h } from 'vue'; 2 | import { createInertiaApp } from '@inertiajs/vue3'; 3 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; 4 | import { ZiggyVue } from '../../vendor/tightenco/ziggy'; 5 | import * as Layouts from '@/layouts'; 6 | import '../css/app.css'; 7 | 8 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; 9 | 10 | console.log('appName', import.meta.env); 11 | 12 | createInertiaApp({ 13 | title: (title) => `${title} - ${appName}`, 14 | resolve: async (name) => { 15 | const page = await resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue') as any); 16 | if (!page.default.layout) { 17 | page.default.layout = Layouts.default; 18 | } 19 | return page; 20 | }, 21 | setup({ el, App, props, plugin }): any { 22 | const app = createApp({ render: () => h(App, props) }) 23 | .use(plugin) 24 | .use(ZiggyVue); 25 | registerPlugins(app); 26 | 27 | app.mount(el); 28 | el.removeAttribute('data-page'); 29 | }, 30 | progress: { 31 | color: 'hsl(var(--primary))', 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /resources/ts/navigation/index.ts: -------------------------------------------------------------------------------- 1 | import { MobileNavItems, NavSection } from '@/types'; 2 | 3 | // This is sample data. 4 | export const navSections: NavSection[] = [ 5 | { 6 | heading: 'General', 7 | navItems: [ 8 | { 9 | title: 'Dashboard', 10 | route: 'dashboard', 11 | icon: 'tabler-home', 12 | }, 13 | { 14 | title: 'Create New Course', 15 | route: 'courses.create', 16 | icon: 'tabler-circle-plus', 17 | }, 18 | { 19 | title: 'Media Library', 20 | route: 'media.index', 21 | icon: 'tabler-library-photo', 22 | }, 23 | ], 24 | }, 25 | ]; 26 | 27 | export const mobileNavItems: MobileNavItems = [ 28 | { icon: 'tabler-home', title: 'Accueil', route: 'dashboard' }, 29 | { icon: 'tabler-search', title: 'Search', route: 'profile.show' }, 30 | { icon: 'tabler-bell', title: 'Notifications', route: 'profile.show' }, 31 | { icon: 'tabler-user', title: 'Profile', route: 'profile.show' }, 32 | ]; 33 | -------------------------------------------------------------------------------- /resources/ts/plugins/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { createI18n } from 'vue-i18n'; 3 | 4 | const messages = Object.fromEntries( 5 | Object.entries(import.meta.glob<{ default: any }>('./locales/*.json', { eager: true })).map(([key, value]) => [key.slice(10, -5), value.default]) 6 | ); 7 | 8 | let _i18n: any = null; 9 | 10 | export const getI18n = () => { 11 | if (_i18n === null) { 12 | _i18n = createI18n({ 13 | legacy: false, 14 | locale: 'en', 15 | fallbackLocale: 'en', 16 | messages, 17 | }); 18 | } 19 | 20 | return _i18n; 21 | }; 22 | 23 | export default function (app: App) { 24 | app.use(getI18n()); 25 | } 26 | -------------------------------------------------------------------------------- /resources/ts/plugins/i18n/vue-i18n.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * global type definitions 3 | * using the typescript interface, you can define the i18n resources that is type-safed! 4 | */ 5 | 6 | /** 7 | * you need to import the some interfaces 8 | */ 9 | import en from '@/plugins/i18n/locales/en.json'; 10 | import 'vue-i18n'; 11 | 12 | type LocaleMessage = typeof en; 13 | 14 | declare module 'vue-i18n' { 15 | export interface DefineLocaleMessage extends LocaleMessage {} 16 | } 17 | -------------------------------------------------------------------------------- /resources/ts/plugins/iconify/index.ts: -------------------------------------------------------------------------------- 1 | import './icons.css'; 2 | 3 | export default function () { 4 | // This plugin just requires icons import 5 | } 6 | -------------------------------------------------------------------------------- /resources/ts/plugins/iconify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | {{-- --}} 12 | {{-- --}} 13 | 14 | 15 | 16 | {{-- --}} 17 | @routes 18 | 19 | @inertiaHead 20 | 21 | 22 | 23 | {{--
24 | 26 |
27 |
28 |
29 |
30 |
31 |
--}} 32 | @inertia 33 | @vite(['resources/ts/main.ts', "resources/ts/Pages/{$page['component']}.vue"]) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /resources/views/emails/team-invitation.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | {{ __('You have been invited to join the :team team!', ['team' => $invitation->team->name]) }} 3 | 4 | @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::registration())) 5 | {{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }} 6 | 7 | @component('mail::button', ['url' => url('/register')]) 8 | {{ __('Create Account') }} 9 | @endcomponent 10 | 11 | {{ __('If you already have an account, you may accept this invitation by clicking the button below:') }} 12 | 13 | @else 14 | {{ __('You may accept this invitation by clicking the button below:') }} 15 | @endif 16 | 17 | 18 | @component('mail::button', ['url' => $acceptUrl]) 19 | {{ __('Accept Invitation') }} 20 | @endcomponent 21 | 22 | {{ __('If you did not expect to receive an invitation to this team, you may discard this email.') }} 23 | @endcomponent 24 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | user(); 8 | })->middleware('auth:sanctum'); 9 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote')->hourly(); 9 | -------------------------------------------------------------------------------- /shims.d.ts: -------------------------------------------------------------------------------- 1 | import { PageProps as InertiaPageProps } from '@inertiajs/core'; 2 | import { route as ziggyRoute } from 'ziggy-js'; 3 | import { AppPageProps } from '@/types'; 4 | 5 | declare global { 6 | /* eslint-disable no-var */ 7 | var route: typeof ziggyRoute; 8 | } 9 | import { ComponentCustomProperties as CCp } from 'vue'; 10 | declare module '@vue/runtime-core' { 11 | interface ComponentCustomProperties { 12 | route: typeof ziggyRoute; 13 | } 14 | } 15 | 16 | declare module '@inertiajs/core' { 17 | interface PageProps extends InertiaPageProps, AppPageProps {} 18 | } 19 | 20 | 21 | interface ImportMetaEnv { 22 | VITE_APP_NAME: string; 23 | VITE_GQL_URI: string; 24 | // others... 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/Feature/ApiTokenPermissionsTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 10 | } else { 11 | $this->actingAs($user = User::factory()->create()); 12 | } 13 | 14 | $token = $user->tokens()->create([ 15 | 'name' => 'Test Token', 16 | 'token' => Str::random(40), 17 | 'abilities' => ['create', 'read'], 18 | ]); 19 | 20 | $this->put('/user/api-tokens/'.$token->id, [ 21 | 'name' => $token->name, 22 | 'permissions' => [ 23 | 'delete', 24 | 'missing-permission', 25 | ], 26 | ]); 27 | 28 | expect($user->fresh()->tokens->first()) 29 | ->can('delete')->toBeTrue() 30 | ->can('read')->toBeFalse() 31 | ->can('missing-permission')->toBeFalse(); 32 | })->skip(function () { 33 | return ! Features::hasApiFeatures(); 34 | }, 'API support is not enabled.'); 35 | -------------------------------------------------------------------------------- /tests/Feature/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('users can authenticate using the login screen', function () { 12 | $user = User::factory()->create(); 13 | 14 | $response = $this->post('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(route('dashboard', absolute: false)); 21 | }); 22 | 23 | test('users cannot authenticate with invalid password', function () { 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/login', [ 27 | 'email' => $user->email, 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $this->assertGuest(); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/Feature/BrowserSessionsTest.php: -------------------------------------------------------------------------------- 1 | actingAs(User::factory()->create()); 7 | 8 | $response = $this->delete('/user/other-browser-sessions', [ 9 | 'password' => 'password', 10 | ]); 11 | 12 | $response->assertSessionHasNoErrors(); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/Feature/CreateApiTokenTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 9 | } else { 10 | $this->actingAs($user = User::factory()->create()); 11 | } 12 | 13 | $this->post('/user/api-tokens', [ 14 | 'name' => 'Test Token', 15 | 'permissions' => [ 16 | 'read', 17 | 'update', 18 | ], 19 | ]); 20 | 21 | expect($user->fresh()->tokens)->toHaveCount(1); 22 | expect($user->fresh()->tokens->first()) 23 | ->name->toEqual('Test Token') 24 | ->can('read')->toBeTrue() 25 | ->can('delete')->toBeFalse(); 26 | })->skip(function () { 27 | return ! Features::hasApiFeatures(); 28 | }, 'API support is not enabled.'); 29 | -------------------------------------------------------------------------------- /tests/Feature/CreateTeamTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 7 | 8 | $this->post('/teams', [ 9 | 'name' => 'Test Team', 10 | ]); 11 | 12 | expect($user->fresh()->ownedTeams)->toHaveCount(2); 13 | expect($user->fresh()->ownedTeams()->latest('id')->first()->name)->toEqual('Test Team'); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/Feature/DeleteAccountTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 8 | 9 | $this->delete('/user', [ 10 | 'password' => 'password', 11 | ]); 12 | 13 | expect($user->fresh())->toBeNull(); 14 | })->skip(function () { 15 | return ! Features::hasAccountDeletionFeatures(); 16 | }, 'Account deletion is not enabled.'); 17 | 18 | test('correct password must be provided before account can be deleted', function () { 19 | $this->actingAs($user = User::factory()->create()); 20 | 21 | $this->delete('/user', [ 22 | 'password' => 'wrong-password', 23 | ]); 24 | 25 | expect($user->fresh())->not->toBeNull(); 26 | })->skip(function () { 27 | return ! Features::hasAccountDeletionFeatures(); 28 | }, 'Account deletion is not enabled.'); 29 | -------------------------------------------------------------------------------- /tests/Feature/DeleteApiTokenTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 10 | } else { 11 | $this->actingAs($user = User::factory()->create()); 12 | } 13 | 14 | $token = $user->tokens()->create([ 15 | 'name' => 'Test Token', 16 | 'token' => Str::random(40), 17 | 'abilities' => ['create', 'read'], 18 | ]); 19 | 20 | $this->delete('/user/api-tokens/'.$token->id); 21 | 22 | expect($user->fresh()->tokens)->toHaveCount(0); 23 | })->skip(function () { 24 | return ! Features::hasApiFeatures(); 25 | }, 'API support is not enabled.'); 26 | -------------------------------------------------------------------------------- /tests/Feature/DeleteTeamTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 8 | 9 | $user->ownedTeams()->save($team = Team::factory()->make([ 10 | 'personal_team' => false, 11 | ])); 12 | 13 | $team->users()->attach( 14 | $otherUser = User::factory()->create(), ['role' => 'test-role'] 15 | ); 16 | 17 | $this->delete('/teams/'.$team->id); 18 | 19 | expect($team->fresh())->toBeNull(); 20 | expect($otherUser->fresh()->teams)->toHaveCount(0); 21 | }); 22 | 23 | test('personal teams cant be deleted', function () { 24 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 25 | 26 | $this->delete('/teams/'.$user->currentTeam->id); 27 | 28 | expect($user->currentTeam->fresh())->not->toBeNull(); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/Feature/InviteTeamMemberTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 12 | 13 | $this->post('/teams/'.$user->currentTeam->id.'/members', [ 14 | 'email' => 'test@example.com', 15 | 'role' => 'admin', 16 | ]); 17 | 18 | Mail::assertSent(TeamInvitation::class); 19 | 20 | expect($user->currentTeam->fresh()->teamInvitations)->toHaveCount(1); 21 | })->skip(function () { 22 | return ! Features::sendsTeamInvitations(); 23 | }, 'Team invitations not enabled.'); 24 | 25 | test('team member invitations can be cancelled', function () { 26 | Mail::fake(); 27 | 28 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 29 | 30 | $invitation = $user->currentTeam->teamInvitations()->create([ 31 | 'email' => 'test@example.com', 32 | 'role' => 'admin', 33 | ]); 34 | 35 | $this->delete('/team-invitations/'.$invitation->id); 36 | 37 | expect($user->currentTeam->fresh()->teamInvitations)->toHaveCount(0); 38 | })->skip(function () { 39 | return ! Features::sendsTeamInvitations(); 40 | }, 'Team invitations not enabled.'); 41 | -------------------------------------------------------------------------------- /tests/Feature/LeaveTeamTest.php: -------------------------------------------------------------------------------- 1 | withPersonalTeam()->create(); 7 | 8 | $user->currentTeam->users()->attach( 9 | $otherUser = User::factory()->create(), ['role' => 'admin'] 10 | ); 11 | 12 | $this->actingAs($otherUser); 13 | 14 | $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id); 15 | 16 | expect($user->currentTeam->fresh()->users)->toHaveCount(0); 17 | }); 18 | 19 | test('team owners cant leave their own team', function () { 20 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 21 | 22 | $response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$user->id); 23 | 24 | $response->assertSessionHasErrorsIn('removeTeamMember', ['team']); 25 | 26 | expect($user->currentTeam->fresh())->not->toBeNull(); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/Feature/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | withPersonalTeam()->create() 9 | : User::factory()->create(); 10 | 11 | $response = $this->actingAs($user)->get('/user/confirm-password'); 12 | 13 | $response->assertStatus(200); 14 | }); 15 | 16 | test('password can be confirmed', function () { 17 | $user = User::factory()->create(); 18 | 19 | $response = $this->actingAs($user)->post('/user/confirm-password', [ 20 | 'password' => 'password', 21 | ]); 22 | 23 | $response->assertRedirect(); 24 | $response->assertSessionHasNoErrors(); 25 | }); 26 | 27 | test('password is not confirmed with invalid password', function () { 28 | $user = User::factory()->create(); 29 | 30 | $response = $this->actingAs($user)->post('/user/confirm-password', [ 31 | 'password' => 'wrong-password', 32 | ]); 33 | 34 | $response->assertSessionHasErrors(); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/Feature/ProfileInformationTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 7 | 8 | $this->put('/user/profile-information', [ 9 | 'name' => 'Test Name', 10 | 'email' => 'test@example.com', 11 | ]); 12 | 13 | expect($user->fresh()) 14 | ->name->toEqual('Test Name') 15 | ->email->toEqual('test@example.com'); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/Feature/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 8 | 9 | $response->assertStatus(200); 10 | })->skip(function () { 11 | return ! Features::enabled(Features::registration()); 12 | }, 'Registration support is not enabled.'); 13 | 14 | test('registration screen cannot be rendered if support is disabled', function () { 15 | $response = $this->get('/register'); 16 | 17 | $response->assertStatus(404); 18 | })->skip(function () { 19 | return Features::enabled(Features::registration()); 20 | }, 'Registration support is enabled.'); 21 | 22 | test('new users can register', function () { 23 | $response = $this->post('/register', [ 24 | 'name' => 'Test User', 25 | 'email' => 'test@example.com', 26 | 'password' => 'password', 27 | 'password_confirmation' => 'password', 28 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(), 29 | ]); 30 | 31 | $this->assertAuthenticated(); 32 | $response->assertRedirect(route('dashboard', absolute: false)); 33 | })->skip(function () { 34 | return ! Features::enabled(Features::registration()); 35 | }, 'Registration support is not enabled.'); 36 | -------------------------------------------------------------------------------- /tests/Feature/RemoveTeamMemberTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 7 | 8 | $user->currentTeam->users()->attach( 9 | $otherUser = User::factory()->create(), ['role' => 'admin'] 10 | ); 11 | 12 | $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id); 13 | 14 | expect($user->currentTeam->fresh()->users)->toHaveCount(0); 15 | }); 16 | 17 | test('only team owner can remove team members', function () { 18 | $user = User::factory()->withPersonalTeam()->create(); 19 | 20 | $user->currentTeam->users()->attach( 21 | $otherUser = User::factory()->create(), ['role' => 'admin'] 22 | ); 23 | 24 | $this->actingAs($otherUser); 25 | 26 | $response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$user->id); 27 | 28 | $response->assertStatus(403); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/Feature/UpdateTeamMemberRoleTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 7 | 8 | $user->currentTeam->users()->attach( 9 | $otherUser = User::factory()->create(), ['role' => 'admin'] 10 | ); 11 | 12 | $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ 13 | 'role' => 'editor', 14 | ]); 15 | 16 | expect($otherUser->fresh()->hasTeamRole( 17 | $user->currentTeam->fresh(), 'editor' 18 | ))->toBeTrue(); 19 | }); 20 | 21 | test('only team owner can update team member roles', function () { 22 | $user = User::factory()->withPersonalTeam()->create(); 23 | 24 | $user->currentTeam->users()->attach( 25 | $otherUser = User::factory()->create(), ['role' => 'admin'] 26 | ); 27 | 28 | $this->actingAs($otherUser); 29 | 30 | $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ 31 | 'role' => 'editor', 32 | ]); 33 | 34 | expect($otherUser->fresh()->hasTeamRole( 35 | $user->currentTeam->fresh(), 'admin' 36 | ))->toBeTrue(); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/Feature/UpdateTeamNameTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 7 | 8 | $this->put('/teams/'.$user->currentTeam->id, [ 9 | 'name' => 'Test Team', 10 | ]); 11 | 12 | expect($user->fresh()->ownedTeams)->toHaveCount(1); 13 | expect($user->currentTeam->fresh()->name)->toEqual('Test Team'); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "isolatedModules": true, 8 | "strict": true, 9 | "jsx": "preserve", 10 | "jsxFactory": "h", 11 | "jsxFragmentFactory": "Fragment", 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "paths": { 15 | "@/*": [ 16 | "./resources/ts/*" 17 | ], 18 | "@core/*": [ 19 | "./resources/ts/core/*" 20 | ], 21 | "@images/*": [ 22 | "./resources/assets/images/*" 23 | ], 24 | }, 25 | "declaration": false, 26 | "outDir": "dist", 27 | "sourceMap": true, 28 | "target": "ESNext", 29 | "types": [ 30 | "vite/client" 31 | ] 32 | }, 33 | "include": [ 34 | "./vite.config.*", 35 | "./env.d.ts", 36 | "./shims.d.ts", 37 | "./resources/ts/**/*", 38 | "./resources/ts/**/*.vue", 39 | "./auto-imports.d.ts", 40 | "./components.d.ts" 41 | ], 42 | "exclude": [ 43 | "./dist", 44 | "./node_modules" 45 | ], 46 | } 47 | --------------------------------------------------------------------------------