From 2399141347f7b1883bab034e836335eab120cde0 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Mon, 13 Oct 2025 14:57:11 +0200 Subject: [PATCH] Undo submodules --- .gitmodules | 6 - backend | 1 - backend/.editorconfig | 18 + backend/.env.example | 71 + backend/.gitattributes | 11 + backend/.gitignore | 24 + backend/Dockerfile | 35 + backend/LICENSE.md | 674 +++ backend/README.md | 14 + .../Commands/GenerateSchedulesCommand.php | 40 + backend/app/Exceptions/CustomException.php | 10 + .../Http/Controllers/Api/ApiController.php | 30 + backend/app/Http/Controllers/Controller.php | 8 + .../app/Http/Middleware/ForceJsonResponse.php | 17 + .../Resources/MinimalScheduleResource.php | 17 + .../MinimalScheduledUserDishResource.php | 33 + .../Resources/ScheduledUserDishResource.php | 26 + .../app/Http/Resources/UserDishResource.php | 24 + backend/app/Http/Resources/UserResource.php | 17 + .../Http/Resources/UserWithDishesResource.php | 30 + .../Resources/UserWithUserDishesResource.php | 33 + backend/app/Models/Dish.php | 64 + backend/app/Models/MinimumRecurrence.php | 27 + backend/app/Models/Planner.php | 31 + backend/app/Models/Schedule.php | 61 + backend/app/Models/ScheduledUserDish.php | 52 + .../app/Models/Scopes/BelongsToPlanner.php | 17 + backend/app/Models/User.php | 80 + backend/app/Models/UserDish.php | 56 + backend/app/Models/UserDishRecurrence.php | 42 + backend/app/Models/WeeklyRecurrence.php | 33 + backend/app/Providers/AppServiceProvider.php | 54 + backend/app/Services/OutputService.php | 26 + backend/app/WeekdaysEnum.php | 14 + backend/artisan | 15 + backend/bin/build_and_push.sh | 3 + backend/bin/update.sh | 18 + backend/bootstrap/app.php | 61 + backend/bootstrap/cache/.gitignore | 2 + backend/bootstrap/providers.php | 5 + backend/composer.json | 76 + backend/config/app.php | 126 + backend/config/auth.php | 103 + backend/config/cache.php | 108 + backend/config/cors.php | 34 + backend/config/database.php | 173 + backend/config/filesystems.php | 77 + backend/config/logging.php | 132 + backend/config/mail.php | 116 + backend/config/queue.php | 112 + backend/config/sanctum.php | 83 + backend/config/services.php | 38 + backend/config/session.php | 217 + backend/database/.gitignore | 1 + backend/database/factories/DishFactory.php | 27 + .../factories/MinimumRecurrenceFactory.php | 26 + backend/database/factories/PlannerFactory.php | 34 + .../database/factories/ScheduleFactory.php | 48 + .../factories/ScheduledUserDishFactory.php | 54 + .../database/factories/UserDishFactory.php | 36 + .../factories/UserDishRecurrenceFactory.php | 38 + backend/database/factories/UserFactory.php | 30 + .../factories/WeeklyRecurrenceFactory.php | 27 + .../0001_01_01_000000_create_users_table.php | 34 + .../0001_01_01_000001_create_cache_table.php | 35 + .../0001_01_01_000002_create_jobs_table.php | 57 + .../2025_01_18_004639_create_dishes_table.php | 23 + .../2025_02_02_130855_user_dishes.php | 27 + ...25_02_08_231219_create_schedules_table.php | 42 + .../2025_03_03_204906_recurrence_types.php | 38 + ...46_create_personal_access_tokens_table.php | 27 + ...025_04_19_195152_create_sessions_table.php | 25 + backend/database/seeders/DatabaseSeeder.php | 18 + backend/database/seeders/DishesSeeder.php | 33 + backend/database/seeders/PlannersSeeder.php | 25 + backend/database/seeders/ScheduleSeeder.php | 57 + backend/database/seeders/UsersSeeder.php | 22 + backend/docker-compose.yml | 52 + backend/package.json | 17 + backend/phpunit.xml | 38 + backend/postcss.config.js | 6 + backend/public/.htaccess | 21 + backend/public/favicon.ico | 0 backend/public/index.php | 17 + backend/public/robots.txt | 2 + backend/resources/css/app.css | 3 + backend/resources/js/app.js | 1 + backend/resources/js/bootstrap.js | 4 + backend/resources/views/welcome.blade.php | 176 + backend/routes/api.php | 16 + backend/routes/api/auth.php | 21 + backend/routes/api/dishes.php | 20 + backend/routes/api/schedule.php | 31 + backend/routes/api/scheduledUserDishes.php | 15 + backend/routes/api/users.php | 40 + backend/routes/console.php | 8 + backend/routes/web.php | 7 + .../Controllers/PlannerAuthController.php | 62 + .../Exceptions/InvalidPlannerException.php | 10 + .../Dish/Actions/AddUsersToDishAction.php | 15 + .../Dish/Actions/CreateDishAction.php | 21 + .../Dish/Actions/DeleteDishAction.php | 15 + .../Actions/RemoveUsersFromDishAction.php | 15 + .../Dish/Actions/SyncUsersAction.php | 15 + .../Dish/Actions/UpdateDishAction.php | 16 + .../Dish/Controllers/DishController.php | 86 + .../Dish/Exceptions/DishNotFoundException.php | 10 + .../Dish/Exceptions/InvalidDishException.php | 11 + .../DishPlanner/Dish/Policies/DishPolicy.php | 44 + .../Dish/Repositories/DishRepository.php | 14 + .../Dish/Requests/AddUsersToDishRequest.php | 16 + .../Requests/RemoveUsersFromDishRequest.php | 16 + .../Dish/Requests/StoreDishRequest.php | 15 + .../Dish/Requests/SyncUsersRequest.php | 16 + .../Dish/Requests/UpdateDishRequest.php | 15 + .../Dish/Resources/DishResource.php | 25 + .../Planner/Actions/CreatePlannerAction.php | 23 + .../Actions/DraftScheduleForDateAction.php | 28 + .../Actions/DraftScheduleForPeriodAction.php | 30 + .../GenerateScheduleForPeriodAction.php | 32 + .../GenerateSchedulesForUserAction.php | 36 + .../Actions/RegenerateScheduleDayAction.php | 18 + .../RegenerateScheduleDayForUserAction.php | 48 + .../Schedule/Actions/UpdateScheduleAction.php | 24 + .../Controllers/ScheduleController.php | 90 + .../ScheduleUserDishController.php | 68 + .../Schedule/Policies/SchedulePolicy.php | 65 + .../Repositories/ScheduleRepository.php | 26 + .../Requests/CreateScheduleRequest.php | 21 + .../Requests/GenerateScheduleRequest.php | 15 + .../Requests/ScheduleUserDishRequest.php | 21 + .../Requests/UpdateScheduleRequest.php | 18 + .../Schedule/Resources/ScheduleResource.php | 44 + .../Schedule/Services/ScheduleGenerator.php | 42 + .../Actions/CreateScheduledUserDishAction.php | 28 + .../Actions/DeleteScheduledUserDishAction.php | 15 + .../Actions/UpdateScheduledUserDishAction.php | 20 + .../ScheduledUserDishController.php | 85 + .../Policies/ScheduledUserDishPolicy.php | 67 + .../ScheduledUserDishRepository.php | 18 + .../UpdateScheduledUserDishRequest.php | 19 + .../User/Actions/CreateUserAction.php | 17 + .../User/Actions/DeleteUserAction.php | 17 + .../User/Actions/DeleteUserDishAction.php | 21 + .../User/Actions/UpdateUserAction.php | 18 + .../User/Controllers/ListUsersController.php | 20 + .../User/Controllers/UserController.php | 60 + .../DishPlanner/User/Policies/UserPolicy.php | 65 + .../User/Requests/CreateUserRequest.php | 15 + .../User/Requests/UpdateUserRequest.php | 15 + .../Actions/CreateFixedRecurrenceAction.php | 33 + .../Actions/CreateMinimumRecurrenceAction.php | 44 + .../UserDish/Actions/CreateUserDishAction.php | 50 + .../Actions/DeleteFixedRecurrenceAction.php | 22 + .../Actions/DeleteMinimumRecurrenceAction.php | 28 + .../SyncRecurrencesForUserDishAction.php | 43 + .../Actions/UpdateFixedRecurrenceAction.php | 32 + .../Actions/UpdateMinimumRecurrenceAction.php | 32 + .../Controllers/ListUserDishesController.php | 26 + .../Controllers/UserDishController.php | 64 + .../UserDishRecurrenceController.php | 98 + .../InvalidRecurrenceTypeException.php | 10 + .../Interfaces/FixedRecurrenceInterface.php | 6 + .../Interfaces/RecurrenceInterface.php | 6 + .../UserDish/Policies/UserDishPolicy.php | 73 + .../Repositories/UserDishRepository.php | 104 + .../Requests/CreateUserDishRequest.php | 22 + .../StoreUserDishRecurrenceRequest.php | 27 + .../UpdateUserDishFixedRecurrenceRequest.php | 35 + backend/storage/app/.gitignore | 4 + backend/storage/app/private/.gitignore | 2 + backend/storage/app/public/.gitignore | 2 + backend/storage/framework/.gitignore | 9 + backend/storage/framework/cache/.gitignore | 3 + .../storage/framework/cache/data/.gitignore | 2 + backend/storage/framework/sessions/.gitignore | 2 + backend/storage/framework/testing/.gitignore | 2 + backend/storage/framework/views/.gitignore | 2 + backend/storage/logs/.gitignore | 2 + backend/tailwind.config.js | 20 + .../tests/Feature/Dish/AddUsersToDishTest.php | 88 + backend/tests/Feature/Dish/CreateDishTest.php | 89 + backend/tests/Feature/Dish/DeleteDishTest.php | 82 + backend/tests/Feature/Dish/ListDishesTest.php | 61 + .../Feature/Dish/RemoveUsersFromDishTest.php | 94 + backend/tests/Feature/Dish/ShowDishTest.php | 51 + .../Feature/Dish/SyncUsersForDishTest.php | 88 + backend/tests/Feature/Dish/UpdateDishTest.php | 80 + backend/tests/Feature/PlannerLoginTest.php | 98 + .../Feature/Schedule/GenerateScheduleTest.php | 257 + .../Feature/Schedule/ListScheduleTest.php | 97 + .../Feature/Schedule/ReadScheduleTest.php | 111 + .../Feature/Schedule/ScheduleUserDishTest.php | 100 + .../Feature/Schedule/UpdateScheduleTest.php | 66 + .../CreateScheduledUserDishTest.php | 121 + .../DeleteScheduledUserDishTest.php | 82 + .../ReadScheduledUserDishTest.php | 130 + .../UpdateScheduledUserDishTest.php | 172 + backend/tests/Feature/User/CreateUserTest.php | 41 + backend/tests/Feature/User/DeleteUserTest.php | 52 + .../Feature/User/Dish/ListUserDishesTest.php | 60 + .../User/Dish/RemoveDishesForUserTest.php | 47 + .../Feature/User/Dish/ShowUserDishTest.php | 78 + .../Dish/StoreRecurrenceForUserDishTest.php | 272 + backend/tests/Feature/User/ListUsersTest.php | 73 + backend/tests/Feature/User/ShowUserTest.php | 54 + .../Feature/User/ShowUserWithDishesTest.php | 59 + backend/tests/Feature/User/UpdateUserTest.php | 60 + backend/tests/TestCase.php | 10 + backend/tests/Traits/DishesTestTrait.php | 25 + backend/tests/Traits/HasPlanner.php | 20 + .../tests/Traits/ScheduledDishesTestTrait.php | 40 + .../RegenerateScheduleDayActionTest.php | 36 + ...RegenerateScheduleDayForUserActionTest.php | 103 + .../DraftScheduleForDateActionTest.php | 41 + .../DraftScheduleForPeriodActionTest.php | 40 + .../Unit/Schedule/ScheduleGeneratorTest.php | 145 + backend/tests/Unit/ScheduleRepositoryTest.php | 43 + .../UpdateScheduledUserDishActionTest.php | 35 + .../Repositories/UserDishRepositoryTest.php | 80 + backend/vite.config.js | 11 + frontend | 1 - frontend/.dockerignore | 4 + frontend/.gitignore | 6 + frontend/Dockerfile | 22 + frontend/LICENSE | 674 +++ frontend/README.md | 87 + frontend/app/app.css | 15 + frontend/app/root.tsx | 75 + frontend/app/routes.ts | 3 + frontend/app/routes/home.tsx | 13 + frontend/app/welcome/logo-dark.svg | 23 + frontend/app/welcome/logo-light.svg | 23 + frontend/app/welcome/welcome.tsx | 89 + frontend/archive/.dockerignore | 1 + frontend/archive/.env.local.example | 2 + frontend/archive/.env.production | 2 + frontend/archive/.gitignore | 45 + frontend/archive/README.md | 2 + frontend/archive/bin/update.sh | 16 + frontend/archive/build_and_push.sh | 3 + frontend/archive/eslint.config.mjs | 16 + frontend/archive/next.config.ts | 15 + frontend/archive/package.json | 33 + frontend/archive/postcss.config.mjs | 8 + frontend/archive/public/dish-planner.webp | Bin 0 -> 237142 bytes frontend/archive/public/file.svg | 1 + frontend/archive/public/globe.svg | 1 + frontend/archive/public/next.svg | 1 + frontend/archive/public/vercel.svg | 1 + frontend/archive/public/window.svg | 1 + .../src/app/dishes/[id]/delete/page.tsx | 83 + .../archive/src/app/dishes/[id]/edit/page.tsx | 59 + .../archive/src/app/dishes/create/page.tsx | 7 + frontend/archive/src/app/dishes/page.tsx | 57 + frontend/archive/src/app/favicon.ico | Bin 0 -> 25931 bytes frontend/archive/src/app/layout.tsx | 31 + frontend/archive/src/app/login/page.tsx | 11 + frontend/archive/src/app/page.tsx | 9 + frontend/archive/src/app/register/page.tsx | 14 + .../src/app/schedule/[date]/edit/page.tsx | 12 + .../scheduled-user-dishes/history/page.tsx | 9 + .../archive/src/app/users/[id]/edit/page.tsx | 30 + .../archive/src/app/users/create/page.tsx | 60 + frontend/archive/src/app/users/page.tsx | 78 + frontend/archive/src/components/Spinner.tsx | 15 + .../components/features/OnboardingBanner.tsx | 49 + .../components/features/auth/LoginForm.tsx | 96 + .../features/auth/RegistrationForm.tsx | 104 + .../features/dishes/AddUserToDishForm.tsx | 101 + .../features/dishes/CreateDishForm.tsx | 95 + .../src/components/features/dishes/Dish.tsx | 39 + .../components/features/dishes/DishCard.tsx | 23 + .../features/dishes/EditDishForm.tsx | 87 + .../dishes/EditDishUserCardEditForm.tsx | 119 + .../features/dishes/RecurrenceLabels.tsx | 54 + .../features/dishes/SyncUsersForm.tsx | 32 + .../features/dishes/UserDishCard.tsx | 83 + .../features/navbar/MobileDropdownMenu.tsx | 69 + .../features/schedule/HistoricalDishes.tsx | 44 + .../features/schedule/ScheduleCalendar.tsx | 75 + .../features/schedule/ScheduleEditForm.tsx | 142 + .../schedule/ScheduleRegenerateButton.tsx | 35 + .../schedule/ScheduleRegenerateForm.tsx | 78 + .../features/schedule/UpcomingDishes.tsx | 62 + .../features/schedule/UserDishEditCard.tsx | 79 + .../features/schedule/dayCard/DateBadge.tsx | 27 + .../schedule/dayCard/ScheduleDayCard.tsx | 50 + .../dayCard/ScheduleDayCardUserDish.tsx | 32 + .../features/users/EditUserForm.tsx | 63 + .../src/components/layout/AuthGuard.tsx | 48 + .../archive/src/components/layout/Card.tsx | 15 + .../archive/src/components/layout/NavBar.tsx | 83 + frontend/archive/src/components/ui/Alert.tsx | 34 + frontend/archive/src/components/ui/Button.tsx | 62 + .../components/ui/Buttons/OutlineButton.tsx | 37 + .../ui/Buttons/OutlineLinkButton.tsx | 49 + .../src/components/ui/Buttons/SolidButton.tsx | 39 + .../components/ui/Buttons/SolidLinkButton.tsx | 46 + .../archive/src/components/ui/Description.tsx | 17 + frontend/archive/src/components/ui/Hr.tsx | 14 + frontend/archive/src/components/ui/Label.tsx | 25 + frontend/archive/src/components/ui/Modal.tsx | 62 + .../archive/src/components/ui/PageTitle.tsx | 18 + .../src/components/ui/RecurrenceInput.tsx | 65 + .../src/components/ui/SectionTitle.tsx | 16 + frontend/archive/src/components/ui/Toggle.tsx | 42 + frontend/archive/src/context/AuthContext.tsx | 46 + frontend/archive/src/helpers/Date.ts | 18 + frontend/archive/src/hooks/useFetchDishes.ts | 22 + frontend/archive/src/hooks/useFetchUsers.ts | 22 + frontend/archive/src/hooks/useRoutes.ts | 32 + frontend/archive/src/styles/base/globals.css | 19 + .../archive/src/styles/components/buttons.css | 42 + .../archive/src/styles/components/select.css | 0 frontend/archive/src/styles/main.css | 10 + frontend/archive/src/styles/theme/borders.css | 14 + frontend/archive/src/styles/theme/colors.css | 10 + .../src/styles/theme/colors/background.css | 226 + .../src/styles/theme/colors/border.css | 286 + .../archive/src/styles/theme/colors/root.css | 193 + .../archive/src/styles/theme/colors/text.css | 216 + frontend/archive/src/styles/theme/fonts.css | 95 + frontend/archive/src/types/DishType.ts | 20 + frontend/archive/src/types/ScheduleType.ts | 27 + .../src/types/ScheduledUserDishType.ts | 21 + frontend/archive/src/types/UserDishType.ts | 8 + frontend/archive/src/types/UserType.ts | 7 + frontend/archive/src/utils/api/apiRequest.ts | 107 + frontend/archive/src/utils/api/auth.ts | 28 + frontend/archive/src/utils/api/dishApi.ts | 149 + frontend/archive/src/utils/api/scheduleApi.ts | 112 + .../src/utils/api/scheduledUserDishesApi.ts | 77 + frontend/archive/src/utils/api/userDishApi.ts | 18 + frontend/archive/src/utils/api/usersApi.ts | 139 + frontend/archive/src/utils/dateBuilder.ts | 24 + frontend/archive/src/utils/scheduleBuilder.ts | 19 + frontend/archive/tailwind.config.ts | 18 + frontend/archive/tsconfig.json | 27 + frontend/package-lock.json | 4828 +++++++++++++++++ frontend/package.json | 30 + frontend/public/favicon.ico | Bin 0 -> 15086 bytes frontend/react-router.config.ts | 7 + frontend/tsconfig.json | 27 + frontend/vite.config.ts | 8 + 345 files changed, 21069 insertions(+), 8 deletions(-) delete mode 100644 .gitmodules delete mode 160000 backend create mode 100644 backend/.editorconfig create mode 100644 backend/.env.example create mode 100644 backend/.gitattributes create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/LICENSE.md create mode 100644 backend/README.md create mode 100644 backend/app/Console/Commands/GenerateSchedulesCommand.php create mode 100644 backend/app/Exceptions/CustomException.php create mode 100755 backend/app/Http/Controllers/Api/ApiController.php create mode 100644 backend/app/Http/Controllers/Controller.php create mode 100644 backend/app/Http/Middleware/ForceJsonResponse.php create mode 100644 backend/app/Http/Resources/MinimalScheduleResource.php create mode 100644 backend/app/Http/Resources/MinimalScheduledUserDishResource.php create mode 100644 backend/app/Http/Resources/ScheduledUserDishResource.php create mode 100644 backend/app/Http/Resources/UserDishResource.php create mode 100644 backend/app/Http/Resources/UserResource.php create mode 100644 backend/app/Http/Resources/UserWithDishesResource.php create mode 100644 backend/app/Http/Resources/UserWithUserDishesResource.php create mode 100755 backend/app/Models/Dish.php create mode 100755 backend/app/Models/MinimumRecurrence.php create mode 100644 backend/app/Models/Planner.php create mode 100644 backend/app/Models/Schedule.php create mode 100644 backend/app/Models/ScheduledUserDish.php create mode 100644 backend/app/Models/Scopes/BelongsToPlanner.php create mode 100644 backend/app/Models/User.php create mode 100644 backend/app/Models/UserDish.php create mode 100755 backend/app/Models/UserDishRecurrence.php create mode 100755 backend/app/Models/WeeklyRecurrence.php create mode 100755 backend/app/Providers/AppServiceProvider.php create mode 100644 backend/app/Services/OutputService.php create mode 100644 backend/app/WeekdaysEnum.php create mode 100644 backend/artisan create mode 100755 backend/bin/build_and_push.sh create mode 100755 backend/bin/update.sh create mode 100644 backend/bootstrap/app.php create mode 100644 backend/bootstrap/cache/.gitignore create mode 100644 backend/bootstrap/providers.php create mode 100644 backend/composer.json create mode 100644 backend/config/app.php create mode 100644 backend/config/auth.php create mode 100644 backend/config/cache.php create mode 100644 backend/config/cors.php create mode 100644 backend/config/database.php create mode 100644 backend/config/filesystems.php create mode 100644 backend/config/logging.php create mode 100644 backend/config/mail.php create mode 100644 backend/config/queue.php create mode 100644 backend/config/sanctum.php create mode 100644 backend/config/services.php create mode 100644 backend/config/session.php create mode 100644 backend/database/.gitignore create mode 100755 backend/database/factories/DishFactory.php create mode 100644 backend/database/factories/MinimumRecurrenceFactory.php create mode 100644 backend/database/factories/PlannerFactory.php create mode 100644 backend/database/factories/ScheduleFactory.php create mode 100644 backend/database/factories/ScheduledUserDishFactory.php create mode 100644 backend/database/factories/UserDishFactory.php create mode 100644 backend/database/factories/UserDishRecurrenceFactory.php create mode 100644 backend/database/factories/UserFactory.php create mode 100644 backend/database/factories/WeeklyRecurrenceFactory.php create mode 100755 backend/database/migrations/0001_01_01_000000_create_users_table.php create mode 100755 backend/database/migrations/0001_01_01_000001_create_cache_table.php create mode 100755 backend/database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100755 backend/database/migrations/2025_01_18_004639_create_dishes_table.php create mode 100755 backend/database/migrations/2025_02_02_130855_user_dishes.php create mode 100755 backend/database/migrations/2025_02_08_231219_create_schedules_table.php create mode 100755 backend/database/migrations/2025_03_03_204906_recurrence_types.php create mode 100755 backend/database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php create mode 100755 backend/database/migrations/2025_04_19_195152_create_sessions_table.php create mode 100644 backend/database/seeders/DatabaseSeeder.php create mode 100644 backend/database/seeders/DishesSeeder.php create mode 100644 backend/database/seeders/PlannersSeeder.php create mode 100755 backend/database/seeders/ScheduleSeeder.php create mode 100644 backend/database/seeders/UsersSeeder.php create mode 100644 backend/docker-compose.yml create mode 100644 backend/package.json create mode 100644 backend/phpunit.xml create mode 100644 backend/postcss.config.js create mode 100644 backend/public/.htaccess create mode 100644 backend/public/favicon.ico create mode 100644 backend/public/index.php create mode 100644 backend/public/robots.txt create mode 100644 backend/resources/css/app.css create mode 100644 backend/resources/js/app.js create mode 100644 backend/resources/js/bootstrap.js create mode 100644 backend/resources/views/welcome.blade.php create mode 100644 backend/routes/api.php create mode 100644 backend/routes/api/auth.php create mode 100644 backend/routes/api/dishes.php create mode 100755 backend/routes/api/schedule.php create mode 100755 backend/routes/api/scheduledUserDishes.php create mode 100644 backend/routes/api/users.php create mode 100644 backend/routes/console.php create mode 100644 backend/routes/web.php create mode 100644 backend/src/DishPlanner/Auth/Controllers/PlannerAuthController.php create mode 100644 backend/src/DishPlanner/Auth/Exceptions/InvalidPlannerException.php create mode 100644 backend/src/DishPlanner/Dish/Actions/AddUsersToDishAction.php create mode 100644 backend/src/DishPlanner/Dish/Actions/CreateDishAction.php create mode 100644 backend/src/DishPlanner/Dish/Actions/DeleteDishAction.php create mode 100644 backend/src/DishPlanner/Dish/Actions/RemoveUsersFromDishAction.php create mode 100644 backend/src/DishPlanner/Dish/Actions/SyncUsersAction.php create mode 100644 backend/src/DishPlanner/Dish/Actions/UpdateDishAction.php create mode 100644 backend/src/DishPlanner/Dish/Controllers/DishController.php create mode 100644 backend/src/DishPlanner/Dish/Exceptions/DishNotFoundException.php create mode 100644 backend/src/DishPlanner/Dish/Exceptions/InvalidDishException.php create mode 100644 backend/src/DishPlanner/Dish/Policies/DishPolicy.php create mode 100644 backend/src/DishPlanner/Dish/Repositories/DishRepository.php create mode 100644 backend/src/DishPlanner/Dish/Requests/AddUsersToDishRequest.php create mode 100644 backend/src/DishPlanner/Dish/Requests/RemoveUsersFromDishRequest.php create mode 100755 backend/src/DishPlanner/Dish/Requests/StoreDishRequest.php create mode 100644 backend/src/DishPlanner/Dish/Requests/SyncUsersRequest.php create mode 100755 backend/src/DishPlanner/Dish/Requests/UpdateDishRequest.php create mode 100755 backend/src/DishPlanner/Dish/Resources/DishResource.php create mode 100644 backend/src/DishPlanner/Planner/Actions/CreatePlannerAction.php create mode 100644 backend/src/DishPlanner/Schedule/Actions/DraftScheduleForDateAction.php create mode 100644 backend/src/DishPlanner/Schedule/Actions/DraftScheduleForPeriodAction.php create mode 100644 backend/src/DishPlanner/Schedule/Actions/GenerateScheduleForPeriodAction.php create mode 100644 backend/src/DishPlanner/Schedule/Actions/GenerateSchedulesForUserAction.php create mode 100644 backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayAction.php create mode 100644 backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayForUserAction.php create mode 100644 backend/src/DishPlanner/Schedule/Actions/UpdateScheduleAction.php create mode 100644 backend/src/DishPlanner/Schedule/Controllers/ScheduleController.php create mode 100644 backend/src/DishPlanner/Schedule/Controllers/ScheduleUserDishController.php create mode 100644 backend/src/DishPlanner/Schedule/Policies/SchedulePolicy.php create mode 100644 backend/src/DishPlanner/Schedule/Repositories/ScheduleRepository.php create mode 100644 backend/src/DishPlanner/Schedule/Requests/CreateScheduleRequest.php create mode 100644 backend/src/DishPlanner/Schedule/Requests/GenerateScheduleRequest.php create mode 100644 backend/src/DishPlanner/Schedule/Requests/ScheduleUserDishRequest.php create mode 100755 backend/src/DishPlanner/Schedule/Requests/UpdateScheduleRequest.php create mode 100755 backend/src/DishPlanner/Schedule/Resources/ScheduleResource.php create mode 100644 backend/src/DishPlanner/Schedule/Services/ScheduleGenerator.php create mode 100644 backend/src/DishPlanner/ScheduledUserDish/Actions/CreateScheduledUserDishAction.php create mode 100644 backend/src/DishPlanner/ScheduledUserDish/Actions/DeleteScheduledUserDishAction.php create mode 100644 backend/src/DishPlanner/ScheduledUserDish/Actions/UpdateScheduledUserDishAction.php create mode 100644 backend/src/DishPlanner/ScheduledUserDish/Controllers/ScheduledUserDishController.php create mode 100644 backend/src/DishPlanner/ScheduledUserDish/Policies/ScheduledUserDishPolicy.php create mode 100644 backend/src/DishPlanner/ScheduledUserDish/Repositories/ScheduledUserDishRepository.php create mode 100644 backend/src/DishPlanner/ScheduledUserDish/Requests/UpdateScheduledUserDishRequest.php create mode 100644 backend/src/DishPlanner/User/Actions/CreateUserAction.php create mode 100644 backend/src/DishPlanner/User/Actions/DeleteUserAction.php create mode 100644 backend/src/DishPlanner/User/Actions/DeleteUserDishAction.php create mode 100644 backend/src/DishPlanner/User/Actions/UpdateUserAction.php create mode 100644 backend/src/DishPlanner/User/Controllers/ListUsersController.php create mode 100644 backend/src/DishPlanner/User/Controllers/UserController.php create mode 100644 backend/src/DishPlanner/User/Policies/UserPolicy.php create mode 100644 backend/src/DishPlanner/User/Requests/CreateUserRequest.php create mode 100644 backend/src/DishPlanner/User/Requests/UpdateUserRequest.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/CreateFixedRecurrenceAction.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/CreateMinimumRecurrenceAction.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/CreateUserDishAction.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/DeleteFixedRecurrenceAction.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/DeleteMinimumRecurrenceAction.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/SyncRecurrencesForUserDishAction.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/UpdateFixedRecurrenceAction.php create mode 100644 backend/src/DishPlanner/UserDish/Actions/UpdateMinimumRecurrenceAction.php create mode 100644 backend/src/DishPlanner/UserDish/Controllers/ListUserDishesController.php create mode 100644 backend/src/DishPlanner/UserDish/Controllers/UserDishController.php create mode 100644 backend/src/DishPlanner/UserDish/Controllers/UserDishRecurrenceController.php create mode 100644 backend/src/DishPlanner/UserDish/Exceptions/InvalidRecurrenceTypeException.php create mode 100644 backend/src/DishPlanner/UserDish/Interfaces/FixedRecurrenceInterface.php create mode 100644 backend/src/DishPlanner/UserDish/Interfaces/RecurrenceInterface.php create mode 100644 backend/src/DishPlanner/UserDish/Policies/UserDishPolicy.php create mode 100644 backend/src/DishPlanner/UserDish/Repositories/UserDishRepository.php create mode 100755 backend/src/DishPlanner/UserDish/Requests/CreateUserDishRequest.php create mode 100644 backend/src/DishPlanner/UserDish/Requests/StoreUserDishRecurrenceRequest.php create mode 100644 backend/src/DishPlanner/UserDish/Requests/UpdateUserDishFixedRecurrenceRequest.php create mode 100644 backend/storage/app/.gitignore create mode 100644 backend/storage/app/private/.gitignore create mode 100644 backend/storage/app/public/.gitignore create mode 100644 backend/storage/framework/.gitignore create mode 100644 backend/storage/framework/cache/.gitignore create mode 100644 backend/storage/framework/cache/data/.gitignore create mode 100644 backend/storage/framework/sessions/.gitignore create mode 100644 backend/storage/framework/testing/.gitignore create mode 100644 backend/storage/framework/views/.gitignore create mode 100755 backend/storage/logs/.gitignore create mode 100644 backend/tailwind.config.js create mode 100755 backend/tests/Feature/Dish/AddUsersToDishTest.php create mode 100755 backend/tests/Feature/Dish/CreateDishTest.php create mode 100644 backend/tests/Feature/Dish/DeleteDishTest.php create mode 100755 backend/tests/Feature/Dish/ListDishesTest.php create mode 100755 backend/tests/Feature/Dish/RemoveUsersFromDishTest.php create mode 100755 backend/tests/Feature/Dish/ShowDishTest.php create mode 100755 backend/tests/Feature/Dish/SyncUsersForDishTest.php create mode 100755 backend/tests/Feature/Dish/UpdateDishTest.php create mode 100644 backend/tests/Feature/PlannerLoginTest.php create mode 100644 backend/tests/Feature/Schedule/GenerateScheduleTest.php create mode 100644 backend/tests/Feature/Schedule/ListScheduleTest.php create mode 100644 backend/tests/Feature/Schedule/ReadScheduleTest.php create mode 100644 backend/tests/Feature/Schedule/ScheduleUserDishTest.php create mode 100644 backend/tests/Feature/Schedule/UpdateScheduleTest.php create mode 100644 backend/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php create mode 100755 backend/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php create mode 100644 backend/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php create mode 100644 backend/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php create mode 100644 backend/tests/Feature/User/CreateUserTest.php create mode 100644 backend/tests/Feature/User/DeleteUserTest.php create mode 100644 backend/tests/Feature/User/Dish/ListUserDishesTest.php create mode 100755 backend/tests/Feature/User/Dish/RemoveDishesForUserTest.php create mode 100644 backend/tests/Feature/User/Dish/ShowUserDishTest.php create mode 100755 backend/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php create mode 100644 backend/tests/Feature/User/ListUsersTest.php create mode 100644 backend/tests/Feature/User/ShowUserTest.php create mode 100644 backend/tests/Feature/User/ShowUserWithDishesTest.php create mode 100644 backend/tests/Feature/User/UpdateUserTest.php create mode 100644 backend/tests/TestCase.php create mode 100644 backend/tests/Traits/DishesTestTrait.php create mode 100644 backend/tests/Traits/HasPlanner.php create mode 100644 backend/tests/Traits/ScheduledDishesTestTrait.php create mode 100644 backend/tests/Unit/Actions/RegenerateScheduleDayActionTest.php create mode 100644 backend/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php create mode 100644 backend/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php create mode 100644 backend/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php create mode 100644 backend/tests/Unit/Schedule/ScheduleGeneratorTest.php create mode 100644 backend/tests/Unit/ScheduleRepositoryTest.php create mode 100644 backend/tests/Unit/UpdateScheduledUserDishActionTest.php create mode 100644 backend/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php create mode 100644 backend/vite.config.js delete mode 160000 frontend create mode 100644 frontend/.dockerignore create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/LICENSE create mode 100644 frontend/README.md create mode 100644 frontend/app/app.css create mode 100644 frontend/app/root.tsx create mode 100644 frontend/app/routes.ts create mode 100644 frontend/app/routes/home.tsx create mode 100644 frontend/app/welcome/logo-dark.svg create mode 100644 frontend/app/welcome/logo-light.svg create mode 100644 frontend/app/welcome/welcome.tsx create mode 100644 frontend/archive/.dockerignore create mode 100644 frontend/archive/.env.local.example create mode 100644 frontend/archive/.env.production create mode 100644 frontend/archive/.gitignore create mode 100644 frontend/archive/README.md create mode 100755 frontend/archive/bin/update.sh create mode 100755 frontend/archive/build_and_push.sh create mode 100644 frontend/archive/eslint.config.mjs create mode 100644 frontend/archive/next.config.ts create mode 100644 frontend/archive/package.json create mode 100644 frontend/archive/postcss.config.mjs create mode 100644 frontend/archive/public/dish-planner.webp create mode 100644 frontend/archive/public/file.svg create mode 100644 frontend/archive/public/globe.svg create mode 100644 frontend/archive/public/next.svg create mode 100644 frontend/archive/public/vercel.svg create mode 100644 frontend/archive/public/window.svg create mode 100644 frontend/archive/src/app/dishes/[id]/delete/page.tsx create mode 100644 frontend/archive/src/app/dishes/[id]/edit/page.tsx create mode 100644 frontend/archive/src/app/dishes/create/page.tsx create mode 100644 frontend/archive/src/app/dishes/page.tsx create mode 100644 frontend/archive/src/app/favicon.ico create mode 100644 frontend/archive/src/app/layout.tsx create mode 100644 frontend/archive/src/app/login/page.tsx create mode 100644 frontend/archive/src/app/page.tsx create mode 100644 frontend/archive/src/app/register/page.tsx create mode 100644 frontend/archive/src/app/schedule/[date]/edit/page.tsx create mode 100644 frontend/archive/src/app/scheduled-user-dishes/history/page.tsx create mode 100644 frontend/archive/src/app/users/[id]/edit/page.tsx create mode 100644 frontend/archive/src/app/users/create/page.tsx create mode 100644 frontend/archive/src/app/users/page.tsx create mode 100644 frontend/archive/src/components/Spinner.tsx create mode 100644 frontend/archive/src/components/features/OnboardingBanner.tsx create mode 100644 frontend/archive/src/components/features/auth/LoginForm.tsx create mode 100644 frontend/archive/src/components/features/auth/RegistrationForm.tsx create mode 100644 frontend/archive/src/components/features/dishes/AddUserToDishForm.tsx create mode 100644 frontend/archive/src/components/features/dishes/CreateDishForm.tsx create mode 100644 frontend/archive/src/components/features/dishes/Dish.tsx create mode 100644 frontend/archive/src/components/features/dishes/DishCard.tsx create mode 100644 frontend/archive/src/components/features/dishes/EditDishForm.tsx create mode 100644 frontend/archive/src/components/features/dishes/EditDishUserCardEditForm.tsx create mode 100644 frontend/archive/src/components/features/dishes/RecurrenceLabels.tsx create mode 100644 frontend/archive/src/components/features/dishes/SyncUsersForm.tsx create mode 100644 frontend/archive/src/components/features/dishes/UserDishCard.tsx create mode 100644 frontend/archive/src/components/features/navbar/MobileDropdownMenu.tsx create mode 100644 frontend/archive/src/components/features/schedule/HistoricalDishes.tsx create mode 100644 frontend/archive/src/components/features/schedule/ScheduleCalendar.tsx create mode 100644 frontend/archive/src/components/features/schedule/ScheduleEditForm.tsx create mode 100644 frontend/archive/src/components/features/schedule/ScheduleRegenerateButton.tsx create mode 100644 frontend/archive/src/components/features/schedule/ScheduleRegenerateForm.tsx create mode 100644 frontend/archive/src/components/features/schedule/UpcomingDishes.tsx create mode 100644 frontend/archive/src/components/features/schedule/UserDishEditCard.tsx create mode 100644 frontend/archive/src/components/features/schedule/dayCard/DateBadge.tsx create mode 100644 frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCard.tsx create mode 100644 frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCardUserDish.tsx create mode 100644 frontend/archive/src/components/features/users/EditUserForm.tsx create mode 100644 frontend/archive/src/components/layout/AuthGuard.tsx create mode 100644 frontend/archive/src/components/layout/Card.tsx create mode 100644 frontend/archive/src/components/layout/NavBar.tsx create mode 100644 frontend/archive/src/components/ui/Alert.tsx create mode 100644 frontend/archive/src/components/ui/Button.tsx create mode 100644 frontend/archive/src/components/ui/Buttons/OutlineButton.tsx create mode 100644 frontend/archive/src/components/ui/Buttons/OutlineLinkButton.tsx create mode 100644 frontend/archive/src/components/ui/Buttons/SolidButton.tsx create mode 100644 frontend/archive/src/components/ui/Buttons/SolidLinkButton.tsx create mode 100644 frontend/archive/src/components/ui/Description.tsx create mode 100644 frontend/archive/src/components/ui/Hr.tsx create mode 100644 frontend/archive/src/components/ui/Label.tsx create mode 100644 frontend/archive/src/components/ui/Modal.tsx create mode 100644 frontend/archive/src/components/ui/PageTitle.tsx create mode 100644 frontend/archive/src/components/ui/RecurrenceInput.tsx create mode 100644 frontend/archive/src/components/ui/SectionTitle.tsx create mode 100644 frontend/archive/src/components/ui/Toggle.tsx create mode 100644 frontend/archive/src/context/AuthContext.tsx create mode 100644 frontend/archive/src/helpers/Date.ts create mode 100644 frontend/archive/src/hooks/useFetchDishes.ts create mode 100644 frontend/archive/src/hooks/useFetchUsers.ts create mode 100644 frontend/archive/src/hooks/useRoutes.ts create mode 100644 frontend/archive/src/styles/base/globals.css create mode 100644 frontend/archive/src/styles/components/buttons.css create mode 100644 frontend/archive/src/styles/components/select.css create mode 100644 frontend/archive/src/styles/main.css create mode 100644 frontend/archive/src/styles/theme/borders.css create mode 100644 frontend/archive/src/styles/theme/colors.css create mode 100644 frontend/archive/src/styles/theme/colors/background.css create mode 100644 frontend/archive/src/styles/theme/colors/border.css create mode 100644 frontend/archive/src/styles/theme/colors/root.css create mode 100644 frontend/archive/src/styles/theme/colors/text.css create mode 100644 frontend/archive/src/styles/theme/fonts.css create mode 100644 frontend/archive/src/types/DishType.ts create mode 100644 frontend/archive/src/types/ScheduleType.ts create mode 100644 frontend/archive/src/types/ScheduledUserDishType.ts create mode 100644 frontend/archive/src/types/UserDishType.ts create mode 100644 frontend/archive/src/types/UserType.ts create mode 100644 frontend/archive/src/utils/api/apiRequest.ts create mode 100644 frontend/archive/src/utils/api/auth.ts create mode 100644 frontend/archive/src/utils/api/dishApi.ts create mode 100644 frontend/archive/src/utils/api/scheduleApi.ts create mode 100644 frontend/archive/src/utils/api/scheduledUserDishesApi.ts create mode 100644 frontend/archive/src/utils/api/userDishApi.ts create mode 100644 frontend/archive/src/utils/api/usersApi.ts create mode 100644 frontend/archive/src/utils/dateBuilder.ts create mode 100644 frontend/archive/src/utils/scheduleBuilder.ts create mode 100644 frontend/archive/tailwind.config.ts create mode 100644 frontend/archive/tsconfig.json create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/react-router.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 48d1fd7..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "backend"] - path = backend - url = git@codeberg.org:DishPlannerCommunity/dish-planner-backend.git -[submodule "frontend"] - path = frontend - url = git@codeberg.org:DishPlannerCommunity/dish-planner-frontend.git diff --git a/backend b/backend deleted file mode 160000 index a7581a0..0000000 --- a/backend +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a7581a0aee52716e8a8bc7f70663cfd6bc96e4c7 diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..11024ef --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,71 @@ +APP_NAME=DishPlanner +APP_ENV=local +APP_KEY=base64:Z3WnYIG9I6xxft15P1EO31WHinj1R36eM/iN3ouyFBM= +APP_DEBUG=true +APP_TIMEZONE=UTC +APP_URL=http://localhost:8000 + +SANCTUM_STATEFUL_DOMAINS=localhost:3000 +SESSION_DOMAIN=localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +WWWGROUP=1000 + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=dishplanner +DB_USERNAME=dpuser +DB_PASSWORD=dppass + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=file +CACHE_PREFIX=dishplanner + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/backend/.gitattributes b/backend/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/backend/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..be5cd4b --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,24 @@ +/composer.lock +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +/auth.json +/.fleet +/.idea +/.nova +/.vscode +/.zed diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..1b43073 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,35 @@ +# Use official PHP base with required extensions +FROM php:8.2-fpm + +# Install system dependencies & PHP extensions +RUN apt-get update && apt-get install -y \ + git unzip curl libzip-dev libpng-dev libonig-dev libxml2-dev zip \ + && docker-php-ext-install pdo pdo_mysql zip mbstring exif pcntl bcmath + +# Install Composer globally +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Set working dir +WORKDIR /var/www + +# Copy app files +COPY . . + +# Install PHP dependencies +RUN composer install --no-dev --optimize-autoloader + +# Laravel optimizations +RUN php artisan config:cache \ + && php artisan route:cache \ + && php artisan view:cache + +# Set correct permissions +RUN chown -R www-data:www-data /var/www \ + && chmod -R 755 /var/www/storage + +USER www-data + +# Expose port 9000 (default for php-fpm) +EXPOSE 9000 + +CMD ["php-fpm"] diff --git a/backend/LICENSE.md b/backend/LICENSE.md new file mode 100644 index 0000000..6b111d1 --- /dev/null +++ b/backend/LICENSE.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..f768271 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,14 @@ +# Dish Planner + +Plan your future dishes + +## Development + +### Installation + +This project uses Laravel Sail, so install all composer packages, get your docker up, and run + +```shell +./vendor/bin/sail up -d +``` + diff --git a/backend/app/Console/Commands/GenerateSchedulesCommand.php b/backend/app/Console/Commands/GenerateSchedulesCommand.php new file mode 100644 index 0000000..98271bd --- /dev/null +++ b/backend/app/Console/Commands/GenerateSchedulesCommand.php @@ -0,0 +1,40 @@ +copy()->addYears(2); + + $this->info("Generating schedules from {$startDate->toDateString()} to {$endDate->toDateString()} for all planners."); + + $planners = Planner::all(); + + if ($planners->isEmpty()) { + $this->warn('No planners found. Aborting schedule generation.'); + return self::FAILURE; + } + + foreach ($planners as $planner) { + $this->info("Processing schedules for Planner ID: {$planner->id}"); + + resolve(GenerateSchedulesForUserAction::class)->execute($planner); + } + + $this->info('Schedule generation for all planners has been completed.'); + + return self::SUCCESS; + } +} diff --git a/backend/app/Exceptions/CustomException.php b/backend/app/Exceptions/CustomException.php new file mode 100644 index 0000000..b5f7bb4 --- /dev/null +++ b/backend/app/Exceptions/CustomException.php @@ -0,0 +1,10 @@ +json(resolve(OutputService::class)->response($success, $payload, $errors), $statusCode); + } + + public function success(?array $payload, int $statusCode = 200): JsonResponse + { + return $this->response(true, $payload, null, $statusCode); + } + + public function error(array|string|null $errors, int $statusCode = 400): JsonResponse + { + return $this->response(false, null, $errors, $statusCode); + } +} diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/backend/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +headers->set('Accept', 'application/json'); + + return $next($request); + } +} diff --git a/backend/app/Http/Resources/MinimalScheduleResource.php b/backend/app/Http/Resources/MinimalScheduleResource.php new file mode 100644 index 0000000..d98cade --- /dev/null +++ b/backend/app/Http/Resources/MinimalScheduleResource.php @@ -0,0 +1,17 @@ + $this->id, + 'date' => $this->date->format('Y-m-d'), + ]; + } +} diff --git a/backend/app/Http/Resources/MinimalScheduledUserDishResource.php b/backend/app/Http/Resources/MinimalScheduledUserDishResource.php new file mode 100644 index 0000000..5772a3b --- /dev/null +++ b/backend/app/Http/Resources/MinimalScheduledUserDishResource.php @@ -0,0 +1,33 @@ + $this->id, + 'schedule' => new MinimalScheduleResource($this->schedule), + 'user' => $this->userDish?->user ? [ + 'id' => $this->userDish->user->id, + 'name' => $this->userDish->user->name, + ] : null, + 'dish' => $this->userDish?->dish ? [ + 'id' => $this->userDish->dish->id, + 'name' => $this->userDish->dish->name, + ] : null, + 'is_skipped' => $this->is_skipped, + ]; + } +} diff --git a/backend/app/Http/Resources/ScheduledUserDishResource.php b/backend/app/Http/Resources/ScheduledUserDishResource.php new file mode 100644 index 0000000..313581a --- /dev/null +++ b/backend/app/Http/Resources/ScheduledUserDishResource.php @@ -0,0 +1,26 @@ + $this->id, + 'schedule' => new MinimalScheduleResource($this->schedule), + 'userDish' => new UserDishResource($this->userDish), + 'is_skipped' => $this->is_skipped, + ]; + } +} diff --git a/backend/app/Http/Resources/UserDishResource.php b/backend/app/Http/Resources/UserDishResource.php new file mode 100644 index 0000000..2f7acb6 --- /dev/null +++ b/backend/app/Http/Resources/UserDishResource.php @@ -0,0 +1,24 @@ + $this->id, + 'user' => new UserResource($this->user), + 'dish' => new DishResource($this->dish), + 'recurrences' => $this->recurrences->map(fn ($recurrence) => [ + 'id' => $recurrence->id, + 'type' => $recurrence->recurrence_type, + 'value' => $recurrence->getValue() + ]), + ]; + } +} diff --git a/backend/app/Http/Resources/UserResource.php b/backend/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..e95e00d --- /dev/null +++ b/backend/app/Http/Resources/UserResource.php @@ -0,0 +1,17 @@ + $this->id, + 'name' => $this->name, + ]; + } +} diff --git a/backend/app/Http/Resources/UserWithDishesResource.php b/backend/app/Http/Resources/UserWithDishesResource.php new file mode 100644 index 0000000..122a34b --- /dev/null +++ b/backend/app/Http/Resources/UserWithDishesResource.php @@ -0,0 +1,30 @@ + $this->id, + 'name' => $this->name, + 'dishes' => $this->mapDishes(), + ]; + } + + private function mapDishes(): array + { + return $this->dishes + ->map(fn (Dish $dish) => [ + 'id' => $dish->id, + 'name' => $dish->name, + 'recurrences' => [], + ]) + ->toArray(); + } +} diff --git a/backend/app/Http/Resources/UserWithUserDishesResource.php b/backend/app/Http/Resources/UserWithUserDishesResource.php new file mode 100644 index 0000000..443968a --- /dev/null +++ b/backend/app/Http/Resources/UserWithUserDishesResource.php @@ -0,0 +1,33 @@ + $this->id, + 'name' => $this->name, + 'user_dishes' => $this->mapDishes(), + ]; + } + + private function mapDishes(): array + { + return $this->userDishes + ->map(fn (UserDish $userDish) => [ + 'id' => $userDish->id, + 'dish' => [ + 'id' => $userDish->dish->id, + 'name' => $userDish->dish->name, + ], + 'recurrences' => [], + ]) + ->toArray(); + } +} diff --git a/backend/app/Models/Dish.php b/backend/app/Models/Dish.php new file mode 100755 index 0000000..a1eb1a2 --- /dev/null +++ b/backend/app/Models/Dish.php @@ -0,0 +1,64 @@ + $users + * @property Collection $userDishes + * @method static create(array $data) + * @method static findOrFail(int $dish_id) + * @method static DishFactory factory($count = null, $state = []) + */ +class Dish extends Model +{ + /** @use HasFactory */ + use HasFactory; + + protected $fillable = ['planner_id', 'name', 'recurrence']; + + protected $casts = []; + + protected static function booted(): void + { + static::addGlobalScope(new BelongsToPlanner); + } + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'user_dishes', 'dish_id', 'user_id') + ->orderBy('name'); + } + + public function userDishes(): HasMany + { + return $this->hasMany(UserDish::class, 'dish_id', 'id'); + } + + public function recurrences(): HasManyThrough + { + return $this->hasManyThrough( + UserDishRecurrence::class, + UserDish::class, + 'dish_id', + 'user_dish_id', + 'id', + 'id' + ); + } +} diff --git a/backend/app/Models/MinimumRecurrence.php b/backend/app/Models/MinimumRecurrence.php new file mode 100755 index 0000000..e91acec --- /dev/null +++ b/backend/app/Models/MinimumRecurrence.php @@ -0,0 +1,27 @@ + */ + use HasFactory; + + protected $fillable = ['days']; + + protected $casts = []; + + public function recurrence(): MorphOne + { + return $this->morphOne(UserDishRecurrence::class, 'recurrence'); + } +} diff --git a/backend/app/Models/Planner.php b/backend/app/Models/Planner.php new file mode 100644 index 0000000..156f26c --- /dev/null +++ b/backend/app/Models/Planner.php @@ -0,0 +1,31 @@ +hasMany(Schedule::class); + } +} diff --git a/backend/app/Models/Schedule.php b/backend/app/Models/Schedule.php new file mode 100644 index 0000000..a055479 --- /dev/null +++ b/backend/app/Models/Schedule.php @@ -0,0 +1,61 @@ + $scheduledUserDishes + * @method static create(array $array) + * @method static Builder where(array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and') + * @method static ScheduleFactory factory($count = null, $state = []) + */ +class Schedule extends Model +{ + /** @use HasFactory */ + use HasFactory; + + protected $table = 'schedules'; + + public $timestamps = false; + + protected $fillable = ['planner_id', 'date', 'is_skipped']; + + protected $casts = [ + 'date' => 'date', + 'is_skipped' => 'boolean', + ]; + + protected static function booted(): void + { + static::addGlobalScope(new BelongsToPlanner); + } + + public function scheduledUserDishes(): HasMany + { + return $this->hasMany(ScheduledUserDish::class); + } + + public function hasAllUsersScheduled(): bool + { + return $this->scheduledUserDishes->count() === User::all()->count(); + } +} diff --git a/backend/app/Models/ScheduledUserDish.php b/backend/app/Models/ScheduledUserDish.php new file mode 100644 index 0000000..0bff736 --- /dev/null +++ b/backend/app/Models/ScheduledUserDish.php @@ -0,0 +1,52 @@ + 'boolean', + ]; + + public function schedule(): BelongsTo + { + return $this->belongsTo(Schedule::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function userDish(): BelongsTo + { + return $this->belongsTo(UserDish::class); + } + + public function scopeForUser(Builder $query, int $userId): Builder + { + return $query->whereHas('userDish', function (Builder $query) use ($userId) { + $query->where('user_id', $userId); + }); + } +} diff --git a/backend/app/Models/Scopes/BelongsToPlanner.php b/backend/app/Models/Scopes/BelongsToPlanner.php new file mode 100644 index 0000000..934aeff --- /dev/null +++ b/backend/app/Models/Scopes/BelongsToPlanner.php @@ -0,0 +1,17 @@ +user()) { + $builder->where('planner_id', $planner->id); + } + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php new file mode 100644 index 0000000..9675e87 --- /dev/null +++ b/backend/app/Models/User.php @@ -0,0 +1,80 @@ + $dishes + * @property Collection $userDishes + * @method static User findOrFail(int $user_id) + * @method static UserFactory factory($count = null, $state = []) + */ +class User extends Authenticatable +{ + /** @use HasFactory */ + use HasFactory, Notifiable; + + protected $fillable = [ + 'planner_id', + 'name', + 'email', + 'password', + ]; + + protected $hidden = [ + 'password', + 'remember_token', + ]; + + protected static function booted(): void + { + static::addGlobalScope(new BelongsToPlanner); + } + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + public function dishes(): BelongsToMany + { + return $this->belongsToMany(Dish::class, 'user_dishes', 'user_id', 'dish_id'); + } + + public function userDishes(): HasMany + { + return $this->hasMany(UserDish::class); + } + + public function recurrences(): HasManyThrough + { + return $this->hasManyThrough( + UserDishRecurrence::class, + UserDish::class, + 'user_id', // Foreign key on user_dishes + 'user_dish_id', // Foreign key on user_dish_recurrences + 'id', // Local key on users + 'id' // Local key on user_dishes + ); + } +} diff --git a/backend/app/Models/UserDish.php b/backend/app/Models/UserDish.php new file mode 100644 index 0000000..a30aab0 --- /dev/null +++ b/backend/app/Models/UserDish.php @@ -0,0 +1,56 @@ + $recurrences + * @property Collection $fixedRecurrences + */ +class UserDish extends Model +{ + use HasFactory; + + protected $fillable = ['user_id', 'dish_id']; + + public function recurrences(): HasMany + { + return $this->hasMany(UserDishRecurrence::class, 'user_dish_id'); + } + + public function fixedRecurrences(): HasMany + { + return $this->hasMany(UserDishRecurrence::class, 'user_dish_id') + ->where('recurrence_type', WeeklyRecurrence::class) + ->join('weekly_recurrences', 'weekly_recurrences.id', '=', 'user_dish_recurrences.recurrence_id') + ->select('weekly_recurrences.*', 'user_dish_recurrences.user_dish_id'); // Ensures we work with weekly_recurrences columns + } + + public function user(): BelongsTo + { + return $this->BelongsTo(User::class); + } + + public function dish(): BelongsTo + { + return $this->BelongsTo(Dish::class); + } +} diff --git a/backend/app/Models/UserDishRecurrence.php b/backend/app/Models/UserDishRecurrence.php new file mode 100755 index 0000000..f968d30 --- /dev/null +++ b/backend/app/Models/UserDishRecurrence.php @@ -0,0 +1,42 @@ + */ + use HasFactory; + + protected $fillable = ['user_dish_id', 'recurrence_id', 'recurrence_type']; + + protected $casts = []; + + public function recurrence(): MorphTo + { + return $this->morphTo(); + } + + public function dishUser(): BelongsTo + { + return $this->belongsTo(UserDish::class, 'user_dish_id', 'id'); + } + + /** + * @throws InvalidRecurrenceTypeException + */ + public function getValue(): int + { + return match ($this->recurrence_type) { + WeeklyRecurrence::class => $this->recurrence->weekday->value, + MinimumRecurrence::class => $this->recurrence->days, + default => throw new InvalidRecurrenceTypeException() + }; + } +} diff --git a/backend/app/Models/WeeklyRecurrence.php b/backend/app/Models/WeeklyRecurrence.php new file mode 100755 index 0000000..fa3b418 --- /dev/null +++ b/backend/app/Models/WeeklyRecurrence.php @@ -0,0 +1,33 @@ + */ + use HasFactory; + + protected $fillable = ['weekday']; + + protected $casts = [ + 'weekday' => WeekdaysEnum::class, + ]; + + public function recurrence(): MorphOne + { + return $this->morphOne(UserDishRecurrence::class, 'recurrence'); + } +} diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php new file mode 100755 index 0000000..0bbbb1e --- /dev/null +++ b/backend/app/Providers/AppServiceProvider.php @@ -0,0 +1,54 @@ +app->bind(ExceptionHandler::class, function ($app) { + return new class($app) extends BaseHandler { + public function render($request, Throwable $e) + { + // Handle specific custom exception + if ($e instanceof CustomException) { + return response()->json([ + 'success' => false, + 'payload' => null, + 'errors' => [$e->getMessage()], + ], $e->getCode() ?? 500); + } + + return parent::render($request, $e); + } + }; + }); + } + + public function boot(): void + { + Gate::policy(Dish::class, DishPolicy::class); + Gate::policy(Schedule::class, SchedulePolicy::class); + Gate::policy(ScheduledUserDish::class, ScheduledUserDishPolicy::class); + Gate::policy(User::class, UserPolicy::class); + Gate::policy(UserDish::class, UserDishPolicy::class); + } +} diff --git a/backend/app/Services/OutputService.php b/backend/app/Services/OutputService.php new file mode 100644 index 0000000..2e7882e --- /dev/null +++ b/backend/app/Services/OutputService.php @@ -0,0 +1,26 @@ + $success, + 'payload' => $payload, + 'errors' => $errors, + ]; + } + + public function success(?array $payload): array + { + return $this->response(true, $payload, null); + } + + public function error(array|string|null $errors): array + { + return $this->response(false, null, $errors); + } +} diff --git a/backend/app/WeekdaysEnum.php b/backend/app/WeekdaysEnum.php new file mode 100644 index 0000000..1f50b3b --- /dev/null +++ b/backend/app/WeekdaysEnum.php @@ -0,0 +1,14 @@ +handleCommand(new ArgvInput); + +exit($status); diff --git a/backend/bin/build_and_push.sh b/backend/bin/build_and_push.sh new file mode 100755 index 0000000..b3ce7d4 --- /dev/null +++ b/backend/bin/build_and_push.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker build -t 192.168.178.152:50114/dishplanner-backend . +docker push 192.168.178.152:50114/dishplanner-backend diff --git a/backend/bin/update.sh b/backend/bin/update.sh new file mode 100755 index 0000000..7320639 --- /dev/null +++ b/backend/bin/update.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +echo "🔄 Pulling latest backend changes..." +git pull origin main + +echo "📦 Installing PHP dependencies..." +composer install --no-interaction --prefer-dist --optimize-autoloader + +echo "🗄️ Running migrations..." +php artisan migrate --force + +echo "🧹 Clearing and caching config..." +php artisan config:cache +php artisan route:cache +php artisan view:cache + +echo "✅ Backend update complete!" diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php new file mode 100644 index 0000000..265ca64 --- /dev/null +++ b/backend/bootstrap/app.php @@ -0,0 +1,61 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + $middleware->append(ForceJsonResponse::class); + $middleware->append(StartSession::class); + $middleware->append(HandleCors::class); + }) + ->withExceptions(function (Exceptions $exceptions) { + $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { + if ($request->is('api/*')) { + return true; + } + + return $request->expectsJson(); + }); + + /** @var OutputService $outputService */ + $outputService = resolve(OutputService::class); + + $exceptions->render(fn (ValidationException $e, Request $request) => $outputService + ->response(false, null, [$e->getMessage()], 404) + ); + + $exceptions->render(fn (NotFoundHttpException $e, Request $request) => response()->json( + $outputService->response(false, null, ['MODEL_NOT_FOUND']), + 404 + )); + + $exceptions->render(fn (AccessDeniedHttpException $e, Request $request) => response()->json( + $outputService->response(false, null, [$e->getMessage()]), + 403 + )); + }) + ->withCommands([ + GenerateScheduleCommand::class, + ]) + ->create(); diff --git a/backend/bootstrap/cache/.gitignore b/backend/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/bootstrap/providers.php b/backend/bootstrap/providers.php new file mode 100644 index 0000000..38b258d --- /dev/null +++ b/backend/bootstrap/providers.php @@ -0,0 +1,5 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => env('APP_TIMEZONE', 'UTC'), + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/backend/config/auth.php b/backend/config/auth.php new file mode 100644 index 0000000..79071c9 --- /dev/null +++ b/backend/config/auth.php @@ -0,0 +1,103 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'planners', + ], + + 'api' => [ + 'driver' => 'session', // or token if you ever need it + 'provider' => 'planners', + ], + ], + + 'providers' => [ + 'planners' => [ + 'driver' => 'eloquent', + 'model' => App\Models\Planner::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/backend/config/cache.php b/backend/config/cache.php new file mode 100644 index 0000000..925f7d2 --- /dev/null +++ b/backend/config/cache.php @@ -0,0 +1,108 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/backend/config/cors.php b/backend/config/cors.php new file mode 100644 index 0000000..94f07be --- /dev/null +++ b/backend/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, + +]; diff --git a/backend/config/database.php b/backend/config/database.php new file mode 100644 index 0000000..125949e --- /dev/null +++ b/backend/config/database.php @@ -0,0 +1,173 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/backend/config/filesystems.php b/backend/config/filesystems.php new file mode 100644 index 0000000..b564035 --- /dev/null +++ b/backend/config/filesystems.php @@ -0,0 +1,77 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/backend/config/logging.php b/backend/config/logging.php new file mode 100644 index 0000000..8d94292 --- /dev/null +++ b/backend/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/backend/config/mail.php b/backend/config/mail.php new file mode 100644 index 0000000..756305b --- /dev/null +++ b/backend/config/mail.php @@ -0,0 +1,116 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/backend/config/queue.php b/backend/config/queue.php new file mode 100644 index 0000000..116bd8d --- /dev/null +++ b/backend/config/queue.php @@ -0,0 +1,112 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/backend/config/sanctum.php b/backend/config/sanctum.php new file mode 100644 index 0000000..764a82f --- /dev/null +++ b/backend/config/sanctum.php @@ -0,0 +1,83 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort() + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/backend/config/services.php b/backend/config/services.php new file mode 100644 index 0000000..27a3617 --- /dev/null +++ b/backend/config/services.php @@ -0,0 +1,38 @@ + [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/backend/config/session.php b/backend/config/session.php new file mode 100644 index 0000000..7ca8b77 --- /dev/null +++ b/backend/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/backend/database/.gitignore b/backend/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/backend/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/backend/database/factories/DishFactory.php b/backend/database/factories/DishFactory.php new file mode 100755 index 0000000..a949383 --- /dev/null +++ b/backend/database/factories/DishFactory.php @@ -0,0 +1,27 @@ + + */ +class DishFactory extends Factory +{ + public function definition(): array + { + return [ + 'name' => fake()->name, + ]; + } + + public function planner(Planner $planner): self + { + return $this->state(fn (array $attributes) => [ + 'planner_id' => $planner->id, + ]); + } +} diff --git a/backend/database/factories/MinimumRecurrenceFactory.php b/backend/database/factories/MinimumRecurrenceFactory.php new file mode 100644 index 0000000..cee95a8 --- /dev/null +++ b/backend/database/factories/MinimumRecurrenceFactory.php @@ -0,0 +1,26 @@ + + */ +class MinimumRecurrenceFactory extends Factory +{ + public function definition(): array + { + return [ + 'days' => fake()->randomElement(range(2, 30)), + ]; + } + + public function days(int $days): self + { + return $this->state(fn (array $attributes) => [ + 'days' => $days, + ]); + } +} diff --git a/backend/database/factories/PlannerFactory.php b/backend/database/factories/PlannerFactory.php new file mode 100644 index 0000000..5b60266 --- /dev/null +++ b/backend/database/factories/PlannerFactory.php @@ -0,0 +1,34 @@ + + */ +class PlannerFactory extends Factory +{ + protected static ?string $password; + + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/backend/database/factories/ScheduleFactory.php b/backend/database/factories/ScheduleFactory.php new file mode 100644 index 0000000..8ed5cbe --- /dev/null +++ b/backend/database/factories/ScheduleFactory.php @@ -0,0 +1,48 @@ + + */ +class ScheduleFactory extends Factory +{ + public function definition(): array + { + return [ + 'planner_id' => null, + 'date' => fake()->dateTimeBetween('-5 years', 'now')->format('Y-m-d'), + 'is_skipped' => false, + ]; + } + + public function planner(Planner $planner): self + { + return $this->state(fn (array $attributes) => [ + 'planner_id' => $planner->id, + ]); + } + + public function date(string|Carbon $date): self + { + if ($date instanceof Carbon) { + $date = $date->format('Y-m-d'); + } + + return $this->state(fn (array $attributes) => [ + 'date' => $date, + ]); + } + + public function skipped(): self + { + return $this->state(fn (array $attributes) => [ + 'is_skipped' => true, + ]); + } +} diff --git a/backend/database/factories/ScheduledUserDishFactory.php b/backend/database/factories/ScheduledUserDishFactory.php new file mode 100644 index 0000000..5dd263e --- /dev/null +++ b/backend/database/factories/ScheduledUserDishFactory.php @@ -0,0 +1,54 @@ + + */ +class ScheduledUserDishFactory extends Factory +{ + public function definition(): array + { + return [ + 'schedule_id' => null, + 'user_id' => null, + 'user_dish_id' => null, + 'is_skipped' => false, + ]; + } + + public function schedule(Schedule $schedule): self + { + return $this->state(fn (array $attributes) => [ + 'schedule_id' => $schedule->id, + ]); + } + + public function user(User $user): self + { + return $this->state(fn (array $attributes) => [ + 'user_id' => $user->id, + ]); + } + + public function userDish(UserDish $userDish): self + { + return $this->state(fn (array $attributes) => [ + 'user_dish_id' => $userDish->id, + 'user_id' => $userDish->user_id, + ]); + } + + public function skipped(): self + { + return $this->state(fn (array $attributes) => [ + 'is_skipped' => true, + ]); + } +} diff --git a/backend/database/factories/UserDishFactory.php b/backend/database/factories/UserDishFactory.php new file mode 100644 index 0000000..7c235ef --- /dev/null +++ b/backend/database/factories/UserDishFactory.php @@ -0,0 +1,36 @@ + + */ +class UserDishFactory extends Factory +{ + public function definition(): array + { + return [ + 'dish_id' => null, + 'user_id' => null, + ]; + } + + public function dish(Dish $dish): self + { + return $this->state(fn (array $attributes) => [ + 'dish_id' => $dish->id, + ]); + } + + public function user(User $user): self + { + return $this->state(fn (array $attributes) => [ + 'user_id' => $user->id, + ]); + } +} diff --git a/backend/database/factories/UserDishRecurrenceFactory.php b/backend/database/factories/UserDishRecurrenceFactory.php new file mode 100644 index 0000000..5a91226 --- /dev/null +++ b/backend/database/factories/UserDishRecurrenceFactory.php @@ -0,0 +1,38 @@ + + */ +class UserDishRecurrenceFactory extends Factory +{ + public function definition(): array + { + return [ + 'user_dish_id' => null, + 'recurrence_id' => null, + 'recurrence_type' => null, + ]; + } + + public function userDish(UserDish $userDish): self + { + return $this->state(fn (array $attributes) => [ + 'user_dish_id' => $userDish->id, + ]); + } + + public function recurrence(RecurrenceInterface $recurrence): self + { + return $this->state(fn (array $attributes) => [ + 'recurrence_id' => $recurrence->id, + 'recurrence_type' => $recurrence::class, + ]); + } +} diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php new file mode 100644 index 0000000..2a2cdd9 --- /dev/null +++ b/backend/database/factories/UserFactory.php @@ -0,0 +1,30 @@ + + */ +class UserFactory extends Factory +{ + protected static ?string $password; + + public function definition(): array + { + return [ + 'planner_id' => null, + 'name' => fake()->name(), + ]; + } + + public function planner(Planner $planner): self + { + return $this->state(fn (array $attributes) => [ + 'planner_id' => $planner->id, + ]); + } +} diff --git a/backend/database/factories/WeeklyRecurrenceFactory.php b/backend/database/factories/WeeklyRecurrenceFactory.php new file mode 100644 index 0000000..3ec2116 --- /dev/null +++ b/backend/database/factories/WeeklyRecurrenceFactory.php @@ -0,0 +1,27 @@ + + */ +class WeeklyRecurrenceFactory extends Factory +{ + public function definition(): array + { + return [ + 'weekday' => fake()->randomElement(range(0, 7)), + ]; + } + + public function weekday(WeekdaysEnum $weekday): self + { + return $this->state(fn (array $attributes) => [ + 'weekday' => $weekday->value, + ]); + } +} diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100755 index 0000000..67773d3 --- /dev/null +++ b/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('users', function (Blueprint $table) { + $table->id(); + $table->foreignId('planner_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('planners'); + } +}; diff --git a/backend/database/migrations/0001_01_01_000001_create_cache_table.php b/backend/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100755 index 0000000..b9c106b --- /dev/null +++ b/backend/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/backend/database/migrations/0001_01_01_000002_create_jobs_table.php b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100755 index 0000000..425e705 --- /dev/null +++ b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/backend/database/migrations/2025_01_18_004639_create_dishes_table.php b/backend/database/migrations/2025_01_18_004639_create_dishes_table.php new file mode 100755 index 0000000..044311e --- /dev/null +++ b/backend/database/migrations/2025_01_18_004639_create_dishes_table.php @@ -0,0 +1,23 @@ +id(); + $table->foreignId('planner_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('dishes'); + } +}; diff --git a/backend/database/migrations/2025_02_02_130855_user_dishes.php b/backend/database/migrations/2025_02_02_130855_user_dishes.php new file mode 100755 index 0000000..4002abe --- /dev/null +++ b/backend/database/migrations/2025_02_02_130855_user_dishes.php @@ -0,0 +1,27 @@ +id(); + $table->unsignedBigInteger('user_id'); + $table->unsignedBigInteger('dish_id'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('dish_id')->references('id')->on('dishes')->onDelete('cascade'); + $table->unique(['user_id', 'dish_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_dishes'); + } +}; diff --git a/backend/database/migrations/2025_02_08_231219_create_schedules_table.php b/backend/database/migrations/2025_02_08_231219_create_schedules_table.php new file mode 100755 index 0000000..98dbb15 --- /dev/null +++ b/backend/database/migrations/2025_02_08_231219_create_schedules_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('planner_id')->constrained()->cascadeOnDelete(); + $table->date('date'); + $table->boolean('is_skipped')->default(false); + + $table->unique(['planner_id', 'date']); + }); + + Schema::create('scheduled_user_dishes', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('schedule_id'); + $table->unsignedBigInteger('user_id'); + $table->unsignedBigInteger('user_dish_id')->nullable(); + $table->boolean('is_skipped')->default(false); + $table->timestamps(); + + $table->foreign('schedule_id')->references('id')->on('schedules')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('user_dish_id')->references('id')->on('user_dishes')->onDelete('cascade'); + + $table->unique(['schedule_id', 'user_dish_id']); + $table->index('user_dish_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('scheduled_user_dishes'); + Schema::dropIfExists('schedules'); + } +}; diff --git a/backend/database/migrations/2025_03_03_204906_recurrence_types.php b/backend/database/migrations/2025_03_03_204906_recurrence_types.php new file mode 100755 index 0000000..39cd07c --- /dev/null +++ b/backend/database/migrations/2025_03_03_204906_recurrence_types.php @@ -0,0 +1,38 @@ +id(); + $table->unsignedBigInteger('user_dish_id'); + $table->integer('recurrence_id'); + $table->string('recurrence_type'); + $table->timestamps(); + + $table->foreign('user_dish_id')->references('id')->on('user_dishes')->cascadeOnDelete(); + }); + + Schema::create('minimum_recurrences', function (Blueprint $table) { + $table->id(); + $table->integer('days'); // Minimum gap in days + $table->timestamps(); + }); + + Schema::create('weekly_recurrences', function (Blueprint $table) { + $table->id(); + $table->integer('weekday'); // 0 = Sunday, 6 = Saturday + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_dish_recurrences'); + } +}; diff --git a/backend/database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php b/backend/database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php new file mode 100755 index 0000000..b30c323 --- /dev/null +++ b/backend/database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php @@ -0,0 +1,27 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/backend/database/migrations/2025_04_19_195152_create_sessions_table.php b/backend/database/migrations/2025_04_19_195152_create_sessions_table.php new file mode 100755 index 0000000..7a84e3d --- /dev/null +++ b/backend/database/migrations/2025_04_19_195152_create_sessions_table.php @@ -0,0 +1,25 @@ +string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sessions'); + } +}; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..3a08996 --- /dev/null +++ b/backend/database/seeders/DatabaseSeeder.php @@ -0,0 +1,18 @@ +call(PlannersSeeder::class); + $this->call(UsersSeeder::class); + $this->call(DishesSeeder::class); + $this->call(ScheduleSeeder::class); + } +} diff --git a/backend/database/seeders/DishesSeeder.php b/backend/database/seeders/DishesSeeder.php new file mode 100644 index 0000000..4c91d47 --- /dev/null +++ b/backend/database/seeders/DishesSeeder.php @@ -0,0 +1,33 @@ +first()], + [$users->last()], + [$users->first(), $users->last()], + ]); + + $planner = Planner::all()->first() ?? Planner::factory()->create(); + + collect([ + 'lasagne', 'pizza', 'burger', 'fries', 'salad', 'sushi', 'pancakes', 'ice cream', 'spaghetti', 'mac and cheese', + 'steak', 'chicken', 'beef', 'pork', 'fish', 'chips', 'cake', + ])->map(fn (string $name) => Dish::factory() + ->create([ + 'planner_id' => $planner->id, + 'name' => $name, + ]) + )->each(fn (Dish $dish) => $dish->users()->attach($userOptions->random())); + } +} diff --git a/backend/database/seeders/PlannersSeeder.php b/backend/database/seeders/PlannersSeeder.php new file mode 100644 index 0000000..2d98cc8 --- /dev/null +++ b/backend/database/seeders/PlannersSeeder.php @@ -0,0 +1,25 @@ + 'Admin', + 'email' => 'admin@test.com', + 'password' => 'password' + ], + ])->each(fn (array $data) => Planner::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + ])); + } +} diff --git a/backend/database/seeders/ScheduleSeeder.php b/backend/database/seeders/ScheduleSeeder.php new file mode 100755 index 0000000..82652c6 --- /dev/null +++ b/backend/database/seeders/ScheduleSeeder.php @@ -0,0 +1,57 @@ +upcoming(); + $this->history(); + } + + public function upcoming(): void + { + $start = Carbon::now()->startOfDay(); + $end = $start->copy()->addDays(14); + + $period = CarbonPeriod::create($start, '1 day', $end); + $this->createScheduleForPeriod($period); + } + + public function history(): void + { + $end = Carbon::now()->subDay()->startOfDay(); + $start = $end->copy()->subDays(14); + + $period = CarbonPeriod::create($start, '1 day', $end); + $this->createScheduleForPeriod($period); + } + + private function createScheduleForPeriod(CarbonPeriod $period): void + { + $planner = Planner::all()->first() ?? Planner::factory()->create(); + + collect($period) + ->each(fn (Carbon $date) => + User::query() + ->inRandomOrder() + ->get() + ->each(fn (User $user) => (new CreateScheduledUserDishAction()) + ->execute( + planner: $planner, + schedule: resolve(ScheduleRepository::class)->findOrCreate($planner, $date), + userDish: $user->userDishes->random(), + ) + ) + ); + } +} diff --git a/backend/database/seeders/UsersSeeder.php b/backend/database/seeders/UsersSeeder.php new file mode 100644 index 0000000..2217a9a --- /dev/null +++ b/backend/database/seeders/UsersSeeder.php @@ -0,0 +1,22 @@ +first() ?? Planner::factory()->create(); + + collect(['Melissa', 'Jochen']) + ->each(fn (string $name) => User::factory()->create([ + 'planner_id' => $planner->id, + 'name' => $name, + ])) + ; + } +} diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..f296bc9 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,52 @@ +services: + laravel.test: + build: + context: './vendor/laravel/sail/runtimes/8.4' + dockerfile: Dockerfile + args: + WWWGROUP: '${WWWGROUP}' + image: 'sail-8.4/app' + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT:-80}:80' + - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + environment: + WWWUSER: '${WWWUSER}' + LARAVEL_SAIL: 1 + XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' + XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' + IGNITION_LOCAL_SITES_PATH: '${PWD}' + volumes: + - '.:/var/www/html' + networks: + - sail + depends_on: + - mysql + mysql: + image: 'mysql/mysql-server:8.0' + ports: + - '${FORWARD_DB_PORT:-3306}:3306' + environment: + MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' + MYSQL_ROOT_HOST: '%' + MYSQL_DATABASE: '${DB_DATABASE}' + MYSQL_ALLOW_EMPTY_PASSWORD: 1 + volumes: + - 'sail-mysql:/var/lib/mysql' + networks: + - sail + healthcheck: + test: + - CMD + - mysqladmin + - ping + - '-p${DB_PASSWORD}' + retries: 3 + timeout: 5s +networks: + sail: + driver: bridge +volumes: + sail-mysql: + driver: local diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..0d10472 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "axios": "^1.7.4", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^1.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "vite": "^6.0" + } +} diff --git a/backend/phpunit.xml b/backend/phpunit.xml new file mode 100644 index 0000000..24bb646 --- /dev/null +++ b/backend/phpunit.xml @@ -0,0 +1,38 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/postcss.config.js b/backend/postcss.config.js new file mode 100644 index 0000000..49c0612 --- /dev/null +++ b/backend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/backend/public/.htaccess b/backend/public/.htaccess new file mode 100644 index 0000000..3aec5e2 --- /dev/null +++ b/backend/public/.htaccess @@ -0,0 +1,21 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/backend/public/favicon.ico b/backend/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/backend/public/index.php b/backend/public/index.php new file mode 100644 index 0000000..947d989 --- /dev/null +++ b/backend/public/index.php @@ -0,0 +1,17 @@ +handleRequest(Request::capture()); diff --git a/backend/public/robots.txt b/backend/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/backend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/backend/resources/css/app.css b/backend/resources/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/backend/resources/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/backend/resources/js/app.js b/backend/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/backend/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/backend/resources/js/bootstrap.js b/backend/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/backend/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/backend/resources/views/welcome.blade.php b/backend/resources/views/welcome.blade.php new file mode 100644 index 0000000..b9d609c --- /dev/null +++ b/backend/resources/views/welcome.blade.php @@ -0,0 +1,176 @@ + + + + + + + Laravel + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + + + + diff --git a/backend/routes/api.php b/backend/routes/api.php new file mode 100644 index 0000000..28b43b3 --- /dev/null +++ b/backend/routes/api.php @@ -0,0 +1,16 @@ + 'api.', +], function () { + require __DIR__ . '/api/auth.php'; + + Route::middleware('auth:sanctum')->group(function () { + require __DIR__ . '/api/users.php'; + require __DIR__ . '/api/dishes.php'; + require __DIR__ . '/api/schedule.php'; + require __DIR__ . '/api/scheduledUserDishes.php'; + }); +}); diff --git a/backend/routes/api/auth.php b/backend/routes/api/auth.php new file mode 100644 index 0000000..bd944e0 --- /dev/null +++ b/backend/routes/api/auth.php @@ -0,0 +1,21 @@ + 'auth.', + 'controller' => PlannerAuthController::class, + 'prefix' => 'auth', +], function () { + Route::post('/login', 'login')->name('login'); + Route::post('/logout', 'logout')->name('logout'); + Route::post('/register', 'register')->name('register'); + + Route::middleware('auth:sanctum') + ->get('/me', fn (Request $request) => response() + ->json($request->user()) + )->name('me'); +}); + diff --git a/backend/routes/api/dishes.php b/backend/routes/api/dishes.php new file mode 100644 index 0000000..2d43513 --- /dev/null +++ b/backend/routes/api/dishes.php @@ -0,0 +1,20 @@ + 'dishes.', + 'controller' => DishController::class, + 'prefix' => 'dishes', +], function () { + Route::get('/', 'index')->name('index'); + Route::post('/', 'store')->name('store'); + Route::get('/{dish}', 'show')->name('show'); + Route::put('/{dish}', 'update')->name('update'); + Route::delete('/{dish}', 'destroy')->name('destroy'); + + Route::post('/{dish}/users/sync', 'syncUsers')->name('users.sync'); + Route::post('/{dish}/users/add', 'addUsers')->name('users.add'); + Route::post('/{dish}/users/remove', 'removeUsers')->name('users.remove'); +}); diff --git a/backend/routes/api/schedule.php b/backend/routes/api/schedule.php new file mode 100755 index 0000000..39e0b8e --- /dev/null +++ b/backend/routes/api/schedule.php @@ -0,0 +1,31 @@ + 'schedule.', + 'controller' => ScheduleController::class, + 'prefix' => 'schedule', +], function () { + Route::get('/', 'index')->name('index'); + Route::get('/{date}', 'show') + ->where('date', '\d{4}-\d{2}-\d{2}') + ->name('show'); + + Route::put('/{date}', 'update') + ->where('date', '\d{4}-\d{2}-\d{2}') + ->name('update'); + + Route::post('/generate', 'generate') + ->name('generate'); + + Route::post('/{date}/user-dishes', ScheduleUserDishController::class) + ->name('user-dish.update'); +}); diff --git a/backend/routes/api/scheduledUserDishes.php b/backend/routes/api/scheduledUserDishes.php new file mode 100755 index 0000000..1a84f8f --- /dev/null +++ b/backend/routes/api/scheduledUserDishes.php @@ -0,0 +1,15 @@ + 'scheduled-user-dishes.', + 'controller' => ScheduledUserDishController::class, + 'prefix' => 'scheduled-user-dishes', +], function () { + Route::post('/', 'create')->name('store'); + Route::get('/{scheduledUserDish}', 'read')->name('show'); + Route::put('/{scheduledUserDish}', 'update')->name('update'); + Route::delete('/{scheduledUserDish}', 'delete')->name('destroy'); +}); diff --git a/backend/routes/api/users.php b/backend/routes/api/users.php new file mode 100644 index 0000000..c75b3dc --- /dev/null +++ b/backend/routes/api/users.php @@ -0,0 +1,40 @@ + 'users.', + 'prefix' => 'users', +], function () { + Route::get('/', ListUsersController::class)->name('index'); + Route::get('/{user}', [UserController::class, 'show'])->name('show'); + Route::post('/', [UserController::class, 'create'])->name('create'); + Route::put('/{user}', [UserController::class, 'update'])->name('update'); + Route::delete('/{user}', [UserController::class, 'delete'])->name('delete'); + + Route::group([ + 'as' => 'dishes.', + 'controller' => UserDishController::class, + 'prefix' => '{user}/dishes', + ], function () { + Route::get('/', 'index')->name('index'); + Route::get('/{dish}', 'show')->name('show'); + Route::post('/{dish}', 'store')->name('store'); + Route::delete('/{dish}', 'destroy')->name('destroy'); + + Route::group([ + 'as' => 'recurrences.', + 'controller' => UserDishRecurrenceController::class, + 'prefix' => '{dish}/recurrences', + ], function () { + Route::post('/', 'store')->name('store'); + }); + }); +}); + +Route::get('/user-dishes', ListUserDishesController::class)->name('user-dishes.index'); diff --git a/backend/routes/console.php b/backend/routes/console.php new file mode 100644 index 0000000..eff2ed2 --- /dev/null +++ b/backend/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote')->hourly(); diff --git a/backend/routes/web.php b/backend/routes/web.php new file mode 100644 index 0000000..86a06c5 --- /dev/null +++ b/backend/routes/web.php @@ -0,0 +1,7 @@ +validate([ + 'email' => ['required', 'email'], + 'password' => ['required'], + ]); + + if (!Auth::attempt($credentials)) { + return response()->json([ + 'message' => 'The provided credentials are incorrect.', + ], 401); + } + + $planner = auth()->user(); + + // Issue token + $token = $planner->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'access_token' => $token, + 'token_type' => 'Bearer', + ]); + } + + public function logout(Request $request): JsonResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return response()->json(['message' => 'Logged out']); + } + + public function register(Request $request) + { + $data = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:planners,email', + 'password' => 'required|string|min:8|confirmed', + ]); + + $planner = resolve(CreatePlannerAction::class)->execute($data); + + return response()->json([ + 'message' => 'Planner registered successfully.', + 'planner' => $planner, + ], 201); + } +} diff --git a/backend/src/DishPlanner/Auth/Exceptions/InvalidPlannerException.php b/backend/src/DishPlanner/Auth/Exceptions/InvalidPlannerException.php new file mode 100644 index 0000000..0252cd3 --- /dev/null +++ b/backend/src/DishPlanner/Auth/Exceptions/InvalidPlannerException.php @@ -0,0 +1,10 @@ +users()->attach($userIds); + + return $dish; + } +} diff --git a/backend/src/DishPlanner/Dish/Actions/CreateDishAction.php b/backend/src/DishPlanner/Dish/Actions/CreateDishAction.php new file mode 100644 index 0000000..4897383 --- /dev/null +++ b/backend/src/DishPlanner/Dish/Actions/CreateDishAction.php @@ -0,0 +1,21 @@ +user(); + + return Dish::create([ + 'planner_id' => $planner->id, + 'name' => Arr::get($data, 'name'), + ]); + } +} diff --git a/backend/src/DishPlanner/Dish/Actions/DeleteDishAction.php b/backend/src/DishPlanner/Dish/Actions/DeleteDishAction.php new file mode 100644 index 0000000..8a52923 --- /dev/null +++ b/backend/src/DishPlanner/Dish/Actions/DeleteDishAction.php @@ -0,0 +1,15 @@ +delete(); + + return true; + } +} diff --git a/backend/src/DishPlanner/Dish/Actions/RemoveUsersFromDishAction.php b/backend/src/DishPlanner/Dish/Actions/RemoveUsersFromDishAction.php new file mode 100644 index 0000000..faa0b54 --- /dev/null +++ b/backend/src/DishPlanner/Dish/Actions/RemoveUsersFromDishAction.php @@ -0,0 +1,15 @@ +users()->detach($userIds); + + return $dish; + } +} diff --git a/backend/src/DishPlanner/Dish/Actions/SyncUsersAction.php b/backend/src/DishPlanner/Dish/Actions/SyncUsersAction.php new file mode 100644 index 0000000..b7c746b --- /dev/null +++ b/backend/src/DishPlanner/Dish/Actions/SyncUsersAction.php @@ -0,0 +1,15 @@ +users()->sync($userIds); + + return $dish; + } +} diff --git a/backend/src/DishPlanner/Dish/Actions/UpdateDishAction.php b/backend/src/DishPlanner/Dish/Actions/UpdateDishAction.php new file mode 100644 index 0000000..2eacb31 --- /dev/null +++ b/backend/src/DishPlanner/Dish/Actions/UpdateDishAction.php @@ -0,0 +1,16 @@ +update(Arr::only($data, ['name'])); + + return $dish->refresh(); + } +} diff --git a/backend/src/DishPlanner/Dish/Controllers/DishController.php b/backend/src/DishPlanner/Dish/Controllers/DishController.php new file mode 100644 index 0000000..02e183c --- /dev/null +++ b/backend/src/DishPlanner/Dish/Controllers/DishController.php @@ -0,0 +1,86 @@ +success(['dishes' => DishResource::collection(Dish::all())]); + } + + public function store(StoreDishRequest $request): JsonResponse + { + $dish = (new CreateDishAction())->execute($request->validated()); + + return $this->success(['dish' => new DishResource($dish)]); + } + + public function show(Dish $dish): JsonResponse + { + Gate::authorize('view', $dish); + + return $this->success(['dish' => new DishResource($dish)]); + } + + public function update(UpdateDishRequest $request, Dish $dish): JsonResponse + { + Gate::authorize('update', $dish); + + $dish = (new UpdateDishAction())->execute($dish, $request->validated()); + + return $this->success(['dish' => new DishResource($dish)]); + } + + public function destroy(Dish $dish): JsonResponse + { + Gate::authorize('delete', $dish); + + (new DeleteDishAction())->execute($dish); + + return $this->success(null); + } + + public function syncUsers(SyncUsersRequest $request, Dish $dish): JsonResponse + { + (new SyncUsersAction())->execute($dish, Arr::get($request->validated(), 'users', [])); + + return $this->success(['dish' => new DishResource($dish->refresh())]); + } + + public function addUsers(AddUsersToDishRequest $request, Dish $dish): JsonResponse + { + Gate::authorize('update', $dish); + + (new AddUsersToDishAction())->execute($dish, Arr::get($request->validated(), 'users', [])); + + return $this->success(['dish' => new DishResource($dish->refresh())]); + } + + public function removeUsers(RemoveUsersFromDishRequest $request, Dish $dish): JsonResponse + { + (new RemoveUsersFromDishAction())->execute($dish, Arr::get($request->validated(), 'users', [])); + + return $this->success(['dish' => new DishResource($dish->refresh())]); + } +} diff --git a/backend/src/DishPlanner/Dish/Exceptions/DishNotFoundException.php b/backend/src/DishPlanner/Dish/Exceptions/DishNotFoundException.php new file mode 100644 index 0000000..1238f96 --- /dev/null +++ b/backend/src/DishPlanner/Dish/Exceptions/DishNotFoundException.php @@ -0,0 +1,10 @@ +id === $dish->planner_id; + } + + public function create(Planner $planner): bool + { + return false; + } + + public function update(Planner $planner, Dish $dish): bool + { + return $this->view($planner, $dish); + } + + public function delete(Planner $planner, Dish $dish): bool + { + return $this->view($planner, $dish); + } + + public function restore(Planner $planner, Dish $dish): bool + { + return false; + } + + public function forceDelete(Planner $planner, Dish $dish): bool + { + return false; + } +} diff --git a/backend/src/DishPlanner/Dish/Repositories/DishRepository.php b/backend/src/DishPlanner/Dish/Repositories/DishRepository.php new file mode 100644 index 0000000..713354f --- /dev/null +++ b/backend/src/DishPlanner/Dish/Repositories/DishRepository.php @@ -0,0 +1,14 @@ +dishes()->inRandomOrder()->first(); + } +} diff --git a/backend/src/DishPlanner/Dish/Requests/AddUsersToDishRequest.php b/backend/src/DishPlanner/Dish/Requests/AddUsersToDishRequest.php new file mode 100644 index 0000000..159582f --- /dev/null +++ b/backend/src/DishPlanner/Dish/Requests/AddUsersToDishRequest.php @@ -0,0 +1,16 @@ + ['required', 'array'], + 'users.*' => ['required', 'integer', 'exists:users,id'], + ]; + } +} diff --git a/backend/src/DishPlanner/Dish/Requests/RemoveUsersFromDishRequest.php b/backend/src/DishPlanner/Dish/Requests/RemoveUsersFromDishRequest.php new file mode 100644 index 0000000..f48ad2d --- /dev/null +++ b/backend/src/DishPlanner/Dish/Requests/RemoveUsersFromDishRequest.php @@ -0,0 +1,16 @@ + ['required', 'array'], + 'users.*' => ['required', 'integer', 'exists:users,id'], + ]; + } +} diff --git a/backend/src/DishPlanner/Dish/Requests/StoreDishRequest.php b/backend/src/DishPlanner/Dish/Requests/StoreDishRequest.php new file mode 100755 index 0000000..6698146 --- /dev/null +++ b/backend/src/DishPlanner/Dish/Requests/StoreDishRequest.php @@ -0,0 +1,15 @@ + ['required', 'string', 'min:3', 'max:128'], + ]; + } +} diff --git a/backend/src/DishPlanner/Dish/Requests/SyncUsersRequest.php b/backend/src/DishPlanner/Dish/Requests/SyncUsersRequest.php new file mode 100644 index 0000000..0d9c9ba --- /dev/null +++ b/backend/src/DishPlanner/Dish/Requests/SyncUsersRequest.php @@ -0,0 +1,16 @@ + ['required', 'array'], + 'users.*' => ['required', 'integer', 'exists:users,id'], + ]; + } +} diff --git a/backend/src/DishPlanner/Dish/Requests/UpdateDishRequest.php b/backend/src/DishPlanner/Dish/Requests/UpdateDishRequest.php new file mode 100755 index 0000000..acf1f6f --- /dev/null +++ b/backend/src/DishPlanner/Dish/Requests/UpdateDishRequest.php @@ -0,0 +1,15 @@ + 'required|string|max:255', + ]; + } +} diff --git a/backend/src/DishPlanner/Dish/Resources/DishResource.php b/backend/src/DishPlanner/Dish/Resources/DishResource.php new file mode 100755 index 0000000..af69c71 --- /dev/null +++ b/backend/src/DishPlanner/Dish/Resources/DishResource.php @@ -0,0 +1,25 @@ + $this->id, + 'planner_id' => $this->planner_id, + 'name' => $this->name, + 'users' => $this->userDishes + ->map(fn (UserDish $userDish) => $userDish->user) + ->map(fn (User $user) => new UserResource($user)) + ->toArray(), + ]; + } +} diff --git a/backend/src/DishPlanner/Planner/Actions/CreatePlannerAction.php b/backend/src/DishPlanner/Planner/Actions/CreatePlannerAction.php new file mode 100644 index 0000000..a3d244f --- /dev/null +++ b/backend/src/DishPlanner/Planner/Actions/CreatePlannerAction.php @@ -0,0 +1,23 @@ + $data['name'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + ]); + + resolve(GenerateSchedulesForUserAction::class)->execute($planner); + + return $planner; + } +} diff --git a/backend/src/DishPlanner/Schedule/Actions/DraftScheduleForDateAction.php b/backend/src/DishPlanner/Schedule/Actions/DraftScheduleForDateAction.php new file mode 100644 index 0000000..1375ad5 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Actions/DraftScheduleForDateAction.php @@ -0,0 +1,28 @@ +reject(fn($user) => $schedule + ->scheduledUserDishes + ->map(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish?->user) + ->filter() + ->contains($user) + ) + ->each(fn (User $user) => $user->userDishes->isNotEmpty() && ScheduledUserDish::create([ + 'schedule_id' => $schedule->id, + 'user_dish_id' => $user->userDishes->random()->id, + 'user_id' => $user->id, + ])); + + return $schedule->refresh(); + } +} diff --git a/backend/src/DishPlanner/Schedule/Actions/DraftScheduleForPeriodAction.php b/backend/src/DishPlanner/Schedule/Actions/DraftScheduleForPeriodAction.php new file mode 100644 index 0000000..9891af2 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Actions/DraftScheduleForPeriodAction.php @@ -0,0 +1,30 @@ +each(function (Carbon $date) use ($planner) { + $schedule = Schedule::query() + ->where('date', $date->format('Y-m-d')) + ->first(); + + if (is_null($schedule)) { + $schedule = Schedule::create([ + 'planner_id' => $planner->id, + 'date' => $date, + ]); + } + + return resolve(DraftScheduleForDateAction::class)->execute($schedule); + }); + } +} diff --git a/backend/src/DishPlanner/Schedule/Actions/GenerateScheduleForPeriodAction.php b/backend/src/DishPlanner/Schedule/Actions/GenerateScheduleForPeriodAction.php new file mode 100644 index 0000000..1e25791 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Actions/GenerateScheduleForPeriodAction.php @@ -0,0 +1,32 @@ +=', Carbon::now())->delete(); + } + + $startDate = Carbon::now(); + $endDate = Carbon::now()->addDays(13); + $period = CarbonPeriod::create($startDate, $endDate); + + /** @var ScheduleRepository $scheduleRepository */ + $scheduleRepository = resolve(ScheduleRepository::class); + + foreach ($period as $date) { + $schedule = $scheduleRepository->findOrCreate($planner, $date); + + resolve(RegenerateScheduleDayAction::class)->execute($planner, $schedule, $overwrite); + } + } +} diff --git a/backend/src/DishPlanner/Schedule/Actions/GenerateSchedulesForUserAction.php b/backend/src/DishPlanner/Schedule/Actions/GenerateSchedulesForUserAction.php new file mode 100644 index 0000000..6b188d1 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Actions/GenerateSchedulesForUserAction.php @@ -0,0 +1,36 @@ +copy()->addYears(2); + + $currentDate = $startDate->copy(); + + while ($currentDate->lte($endDate)) { + $exists = Schedule::where('planner_id', $planner->id) + ->where('date', $currentDate->toDateString()) + ->exists(); + + if (! $exists) { + Schedule::create([ + 'planner_id' => $planner->id, + 'date' => $currentDate->toDateString(), + 'is_skipped' => false, + ]); + } + + $currentDate->addDay(); + } + + return 0; + } +} diff --git a/backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayAction.php b/backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayAction.php new file mode 100644 index 0000000..e4416fe --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayAction.php @@ -0,0 +1,18 @@ +each(fn (User $user) => resolve(RegenerateScheduleDayForUserAction::class) + ->execute($planner, $schedule, $user, $overwrite) + ); + } +} diff --git a/backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayForUserAction.php b/backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayForUserAction.php new file mode 100644 index 0000000..a27de32 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Actions/RegenerateScheduleDayForUserAction.php @@ -0,0 +1,48 @@ +findForScheduleAndUser($schedule, $user); + + $fixedUserDish = resolve(UserDishRepository::class)->findForUserByFixedRecurrenceOnDay($user, $schedule->date->dayOfWeek); + + /** @var CreateScheduledUserDishAction $createAction */ + $createAction = resolve(CreateScheduledUserDishAction::class); + + if (is_null($scheduledUserDish)) { + return $createAction->execute( + planner: $planner, + schedule: $schedule, + userDish: $fixedUserDish ?? $user->userDishes->random(), + ); + } + + if (!$overwrite && $scheduledUserDish->userDish) { + return $scheduledUserDish; + } + + if ($fixedUserDish && $scheduledUserDish->user_dish_id === $fixedUserDish->id) { + return $scheduledUserDish; + } + + $scheduledUserDish->delete(); + + return $createAction->execute( + planner: $planner, + schedule: $schedule, + userDish: $fixedUserDish ?? $user->userDishes->random(), + ); + } +} diff --git a/backend/src/DishPlanner/Schedule/Actions/UpdateScheduleAction.php b/backend/src/DishPlanner/Schedule/Actions/UpdateScheduleAction.php new file mode 100644 index 0000000..db06acf --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Actions/UpdateScheduleAction.php @@ -0,0 +1,24 @@ +update([ + 'is_skipped' => $isSkipped, + ]); + + $schedule + ->scheduledUserDishes + ->each(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish + ->update(['user_dish_id' => null]) + ); + + return $schedule->refresh(); + } +} diff --git a/backend/src/DishPlanner/Schedule/Controllers/ScheduleController.php b/backend/src/DishPlanner/Schedule/Controllers/ScheduleController.php new file mode 100644 index 0000000..5dd9f6c --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Controllers/ScheduleController.php @@ -0,0 +1,90 @@ +user(); + + $startDate = $request->get('start', now()->format('Y-m-d')); + $endDate = $request->get('end', now()->addWeeks(2)->format('Y-m-d')); + + $schedule = Schedule::query() + ->where('planner_id', $planner->id) + ->where('date', '>=', $startDate) + ->where('date', '<=', $endDate) + ->orderBy('date', 'asc')->get()->values(); + + return $this->success([ + 'schedule' => ScheduleResource::collection($schedule), + ]); + } + + public function show(Carbon $date): JsonResponse + { + /** @var Planner $planner */ + $planner = auth()->user(); + + /** @var ScheduleRepository $scheduleRepository */ + $scheduleRepository = resolve(ScheduleRepository::class); + $schedule = $scheduleRepository->findOrCreate($planner, $date); + + Gate::authorize('view', $schedule); + + return $this->success([ + 'schedule' => new ScheduleResource($schedule), + ]); + } + + public function update(UpdateScheduleRequest $request, Carbon $date): JsonResponse + { + /** @var Planner $planner */ + $planner = auth()->user(); + + /** @var ScheduleRepository $scheduleRepository */ + $scheduleRepository = resolve(ScheduleRepository::class); + $schedule = $scheduleRepository->findOrCreate($planner, $date); + + Gate::authorize('update', $schedule); + + resolve(UpdateScheduleAction::class)->execute( + schedule: $schedule, + isSkipped: $request->is_skipped + ); + + return $this->success([ + 'schedule' => new ScheduleResource($schedule->refresh()), + ]); + } + + public function generate(GenerateScheduleRequest $request): JsonResponse + { + /** @var Planner $planner */ + $planner = auth()->user(); + + (new GenerateScheduleForPeriodAction())->execute($planner, $request->get('overwrite', false)); + + return $this->success(null); + } +} diff --git a/backend/src/DishPlanner/Schedule/Controllers/ScheduleUserDishController.php b/backend/src/DishPlanner/Schedule/Controllers/ScheduleUserDishController.php new file mode 100644 index 0000000..f94082a --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Controllers/ScheduleUserDishController.php @@ -0,0 +1,68 @@ +user(); + + $skipped = $request->get('skipped', false); + $userId = $request->get('user_id'); + + $user = User::findOrFail($userId); + + /** @var ScheduleRepository $scheduleRepository */ + $scheduleRepository = resolve(ScheduleRepository::class); + $schedule = $scheduleRepository->findOrCreate($planner, $date); + + $userDishId = $request->get('user_dish_id'); + + $scheduledUserDish = $schedule + ->scheduledUserDishes() + ->forUser($user->id) + ->first(); + + if (! $scheduledUserDish) { + $scheduledUserDish = new ScheduledUserDish(); + } + + abort_if( + boolean: $userDishId === null && $skipped === false, + code: 400, + message: 'Skipped is required to be true if no user_dish_id is provided.' + ); + + $userDish = UserDish::find($userDishId); + + if ($userDish) { + abort_if($userDish->user->id !== $user->id, 400, 'User does not match.'); + } + + $scheduledUserDish->fill([ + 'schedule_id' => $schedule->id, + 'user_id' => $user->id, + 'user_dish_id' => $userDish?->id, + 'is_skipped' => is_null($userDish) && $skipped, + ]); + + $scheduledUserDish->save(); + + return $this->success([ + 'schedule' => new ScheduleResource($schedule->refresh()), + ]); + } +} diff --git a/backend/src/DishPlanner/Schedule/Policies/SchedulePolicy.php b/backend/src/DishPlanner/Schedule/Policies/SchedulePolicy.php new file mode 100644 index 0000000..b8352d1 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Policies/SchedulePolicy.php @@ -0,0 +1,65 @@ +planner_id === $planner->id; + } + + /** + * Determine whether the user can create models. + */ + public function create(Planner $planner): bool + { + return false; + } + + /** + * Determine whether the user can update the model. + */ + public function update(Planner $planner, Schedule $schedule): bool + { + return $this->view($planner, $schedule); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Planner $planner, Schedule $schedule): bool + { + return false; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Planner $planner, Schedule $schedule): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(Planner $planner, Schedule $schedule): bool + { + return false; + } +} diff --git a/backend/src/DishPlanner/Schedule/Repositories/ScheduleRepository.php b/backend/src/DishPlanner/Schedule/Repositories/ScheduleRepository.php new file mode 100644 index 0000000..8c0cca8 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Repositories/ScheduleRepository.php @@ -0,0 +1,26 @@ +id) + ->where('date', $date->format('Y-m-d')) + ->first(); + + if (is_null($schedule)) { + $schedule = Schedule::create([ + 'planner_id' => $planner->id, + 'date' => $date->format('Y-m-d'), + ]); + } + + return $schedule; + } +} diff --git a/backend/src/DishPlanner/Schedule/Requests/CreateScheduleRequest.php b/backend/src/DishPlanner/Schedule/Requests/CreateScheduleRequest.php new file mode 100644 index 0000000..30d6ebe --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Requests/CreateScheduleRequest.php @@ -0,0 +1,21 @@ + 'required|exists:user_dishes,id', + 'date' => 'required|date|date_format:Y-m-d|after:today', + ]; + } +} diff --git a/backend/src/DishPlanner/Schedule/Requests/GenerateScheduleRequest.php b/backend/src/DishPlanner/Schedule/Requests/GenerateScheduleRequest.php new file mode 100644 index 0000000..e48901e --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Requests/GenerateScheduleRequest.php @@ -0,0 +1,15 @@ + ['sometimes', 'boolean'], + ]; + } +} diff --git a/backend/src/DishPlanner/Schedule/Requests/ScheduleUserDishRequest.php b/backend/src/DishPlanner/Schedule/Requests/ScheduleUserDishRequest.php new file mode 100644 index 0000000..94f845a --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Requests/ScheduleUserDishRequest.php @@ -0,0 +1,21 @@ + [ + 'required_without:skipped', + 'exists:user_dishes,id', + 'nullable' + ], + 'user_id' => ['required', 'exists:users,id'], + 'skipped' => ['required_if:user_dish_id,null', 'boolean'], + ]; + } +} diff --git a/backend/src/DishPlanner/Schedule/Requests/UpdateScheduleRequest.php b/backend/src/DishPlanner/Schedule/Requests/UpdateScheduleRequest.php new file mode 100755 index 0000000..895515e --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Requests/UpdateScheduleRequest.php @@ -0,0 +1,18 @@ + ['required', 'boolean'], + ]; + } +} diff --git a/backend/src/DishPlanner/Schedule/Resources/ScheduleResource.php b/backend/src/DishPlanner/Schedule/Resources/ScheduleResource.php new file mode 100755 index 0000000..dde8164 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Resources/ScheduleResource.php @@ -0,0 +1,44 @@ + $scheduledUserDishes + */ +class ScheduleResource extends JsonResource +{ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'date' => $this->date->format('Y-m-d'), + 'is_skipped' => $this->is_skipped, + 'scheduled_user_dishes' => $this->scheduledUserDishes(), + ]; + } + + private function scheduledUserDishes(): array + { + return $this->scheduledUserDishes + ->map(fn (ScheduledUserDish $scheduledUserDish) => [ + 'id' => $scheduledUserDish->id, + 'user' => [ + 'id' => $scheduledUserDish->user->id, + 'name' => $scheduledUserDish->user->name, + ], + 'skipped' => $scheduledUserDish->is_skipped, + 'user_dish' => new UserDishResource($scheduledUserDish->userDish), + ]) + ->toArray(); + } +} diff --git a/backend/src/DishPlanner/Schedule/Services/ScheduleGenerator.php b/backend/src/DishPlanner/Schedule/Services/ScheduleGenerator.php new file mode 100644 index 0000000..5e86f48 --- /dev/null +++ b/backend/src/DishPlanner/Schedule/Services/ScheduleGenerator.php @@ -0,0 +1,42 @@ +startOfDay()->addWeeks(2)); + + /** @var ScheduleRepository $scheduleRepository */ + $scheduleRepository = resolve(ScheduleRepository::class); + + /** @var UserDishRepository $userDishRepository */ + $userDishRepository = resolve(UserDishRepository::class); + + foreach ($period as $date) { + $users->each(function (User $user) use ($date, $planner, $scheduleRepository, $userDishRepository) { + $schedule = $scheduleRepository->findOrCreate($planner, $date); + + (new CreateScheduledUserDishAction())->execute( + planner: $planner, + schedule: $schedule, + userDish: $userDishRepository->getRandomForDate($user, $date) + ); + }); + } + } +} diff --git a/backend/src/DishPlanner/ScheduledUserDish/Actions/CreateScheduledUserDishAction.php b/backend/src/DishPlanner/ScheduledUserDish/Actions/CreateScheduledUserDishAction.php new file mode 100644 index 0000000..9ab9994 --- /dev/null +++ b/backend/src/DishPlanner/ScheduledUserDish/Actions/CreateScheduledUserDishAction.php @@ -0,0 +1,28 @@ +dish->planner_id !== $planner->id || $userDish->user->planner_id !== $planner->id) { + throw new InvalidPlannerException(); + } + + return ScheduledUserDish::create([ + 'schedule_id' => $schedule->id, + 'user_dish_id' => $userDish->id, + 'user_id' => $userDish->user_id, + ]); + } +} diff --git a/backend/src/DishPlanner/ScheduledUserDish/Actions/DeleteScheduledUserDishAction.php b/backend/src/DishPlanner/ScheduledUserDish/Actions/DeleteScheduledUserDishAction.php new file mode 100644 index 0000000..6789ef8 --- /dev/null +++ b/backend/src/DishPlanner/ScheduledUserDish/Actions/DeleteScheduledUserDishAction.php @@ -0,0 +1,15 @@ +delete(); + + return true; + } +} diff --git a/backend/src/DishPlanner/ScheduledUserDish/Actions/UpdateScheduledUserDishAction.php b/backend/src/DishPlanner/ScheduledUserDish/Actions/UpdateScheduledUserDishAction.php new file mode 100644 index 0000000..e13c5f8 --- /dev/null +++ b/backend/src/DishPlanner/ScheduledUserDish/Actions/UpdateScheduledUserDishAction.php @@ -0,0 +1,20 @@ +is_skipped = true; + } + + $scheduledUserDish->user_dish_id = $userDish?->id; + $scheduledUserDish->save(); + } +} diff --git a/backend/src/DishPlanner/ScheduledUserDish/Controllers/ScheduledUserDishController.php b/backend/src/DishPlanner/ScheduledUserDish/Controllers/ScheduledUserDishController.php new file mode 100644 index 0000000..db4b912 --- /dev/null +++ b/backend/src/DishPlanner/ScheduledUserDish/Controllers/ScheduledUserDishController.php @@ -0,0 +1,85 @@ +user_dish_id); + + Gate::authorize('assign', $userDish); + + $date = Carbon::createFromFormat('Y-m-d', $request->date); + + /** @var Planner $planner */ + $planner = auth()->user(); + + $schedule = resolve(ScheduleRepository::class)->findOrCreate($planner, $date); + + try { + $scheduledUserDish = (new CreateScheduledUserDishAction())->execute( + planner: $planner, + schedule: $schedule, + userDish: $userDish, + ); + } catch (InvalidPlannerException $e) { + return $this->error($e->getMessage(), 404); + } + + return $this->success([ + 'scheduled_user_dish' => new ScheduledUserDishResource($scheduledUserDish) + ]); + } + + public function read(ScheduledUserDish $scheduledUserDish): JsonResponse + { + Gate::authorize('view', $scheduledUserDish); + + return $this->success([ + 'scheduled_user_dish' => new ScheduledUserDishResource($scheduledUserDish->refresh()), + ]); + } + + public function update(UpdateScheduledUserDishRequest $request, ScheduledUserDish $scheduledUserDish): JsonResponse + { + Gate::authorize('update', $scheduledUserDish); + + (new UpdateScheduledUserDishAction())->execute( + scheduledUserDish: $scheduledUserDish, + userDish: UserDish::find($request->user_dish_id), + isSkipped: $request->is_skipped ?? null, + ); + + return $this->success([ + 'scheduled_user_dish' => new ScheduledUserDishResource($scheduledUserDish->refresh()), + ]); + } + + public function delete(ScheduledUserDish $scheduledUserDish): JsonResponse + { + Gate::authorize('delete', $scheduledUserDish); + + (new DeleteScheduledUserDishAction())->execute($scheduledUserDish); + + return $this->success(null); + } +} diff --git a/backend/src/DishPlanner/ScheduledUserDish/Policies/ScheduledUserDishPolicy.php b/backend/src/DishPlanner/ScheduledUserDish/Policies/ScheduledUserDishPolicy.php new file mode 100644 index 0000000..dafe218 --- /dev/null +++ b/backend/src/DishPlanner/ScheduledUserDish/Policies/ScheduledUserDishPolicy.php @@ -0,0 +1,67 @@ +exists; + } + + /** + * Determine whether the user can view the model. + */ + public function view(Planner $planner, ScheduledUserDish $scheduledUserDish): bool + { + return Gate::forUser($planner)->allows('view', $scheduledUserDish->userDish); + } + + /** + * Determine whether the user can create models. + */ + public function create(Planner $planner): bool + { + return $this->viewAny($planner); + } + + /** + * Determine whether the user can update the model. + */ + public function update(Planner $planner, ScheduledUserDish $scheduledUserDish): bool + { + return $this->view($planner, $scheduledUserDish); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Planner $planner, ScheduledUserDish $scheduledUserDish): bool + { + return Gate::forUser($planner)->allows('delete', $scheduledUserDish->userDish); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Planner $planner, ScheduledUserDish $scheduledUserDish): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(Planner $planner, ScheduledUserDish $scheduledUserDish): bool + { + return false; + } +} diff --git a/backend/src/DishPlanner/ScheduledUserDish/Repositories/ScheduledUserDishRepository.php b/backend/src/DishPlanner/ScheduledUserDish/Repositories/ScheduledUserDishRepository.php new file mode 100644 index 0000000..fa7fb26 --- /dev/null +++ b/backend/src/DishPlanner/ScheduledUserDish/Repositories/ScheduledUserDishRepository.php @@ -0,0 +1,18 @@ +where('schedule_id', $schedule->id) + ->whereHas('userDish', fn ($query) => $query->where('user_id', $user->id)) + ->first(); + } +} diff --git a/backend/src/DishPlanner/ScheduledUserDish/Requests/UpdateScheduledUserDishRequest.php b/backend/src/DishPlanner/ScheduledUserDish/Requests/UpdateScheduledUserDishRequest.php new file mode 100644 index 0000000..506d311 --- /dev/null +++ b/backend/src/DishPlanner/ScheduledUserDish/Requests/UpdateScheduledUserDishRequest.php @@ -0,0 +1,19 @@ + ['nullable', 'exists:user_dishes,id', 'required_unless:is_skipped,true'], + 'is_skipped' => ['nullable', 'boolean'], + ]; + } +} diff --git a/backend/src/DishPlanner/User/Actions/CreateUserAction.php b/backend/src/DishPlanner/User/Actions/CreateUserAction.php new file mode 100644 index 0000000..f54e814 --- /dev/null +++ b/backend/src/DishPlanner/User/Actions/CreateUserAction.php @@ -0,0 +1,17 @@ + $planner->id, + 'name' => $name, + ]); + } +} diff --git a/backend/src/DishPlanner/User/Actions/DeleteUserAction.php b/backend/src/DishPlanner/User/Actions/DeleteUserAction.php new file mode 100644 index 0000000..a18a120 --- /dev/null +++ b/backend/src/DishPlanner/User/Actions/DeleteUserAction.php @@ -0,0 +1,17 @@ +dishes()->delete(); + $user->delete(); + + return true; + } +} diff --git a/backend/src/DishPlanner/User/Actions/DeleteUserDishAction.php b/backend/src/DishPlanner/User/Actions/DeleteUserDishAction.php new file mode 100644 index 0000000..038acd0 --- /dev/null +++ b/backend/src/DishPlanner/User/Actions/DeleteUserDishAction.php @@ -0,0 +1,21 @@ +id)->where('dish_id', $dish->id)->first(); + + if (! $userDish) { + return; + } + + $userDish->delete(); + } +} diff --git a/backend/src/DishPlanner/User/Actions/UpdateUserAction.php b/backend/src/DishPlanner/User/Actions/UpdateUserAction.php new file mode 100644 index 0000000..c59c77c --- /dev/null +++ b/backend/src/DishPlanner/User/Actions/UpdateUserAction.php @@ -0,0 +1,18 @@ +update([ + 'name' => $name, + ]); + + return $user->refresh(); + } +} diff --git a/backend/src/DishPlanner/User/Controllers/ListUsersController.php b/backend/src/DishPlanner/User/Controllers/ListUsersController.php new file mode 100644 index 0000000..62835d3 --- /dev/null +++ b/backend/src/DishPlanner/User/Controllers/ListUsersController.php @@ -0,0 +1,20 @@ +success(['users' => UserWithUserDishesResource::collection(User::all()->sortBy('id'))]); + } +} diff --git a/backend/src/DishPlanner/User/Controllers/UserController.php b/backend/src/DishPlanner/User/Controllers/UserController.php new file mode 100644 index 0000000..9890708 --- /dev/null +++ b/backend/src/DishPlanner/User/Controllers/UserController.php @@ -0,0 +1,60 @@ +success(['user' => new UserResource($user)]); + } + + public function create(CreateUserRequest $request): JsonResponse + { + /** @var Planner $planner */ + $planner = auth()->user(); + + Gate::authorize('create', User::class); + + $requestData = $request->validated(); + + $user = (new CreateUserAction()) + ->execute($planner, Arr::get($requestData, 'name')); + + return $this->success(['user' => new UserResource($user)]); + } + + public function update(UpdateUserRequest $request, User $user): JsonResponse + { + Gate::authorize('update', $user); + + $user = (new UpdateUserAction()) + ->execute($user, Arr::get($request->validated(), 'name')); + + return $this->success(['user' => new UserResource($user)]); + } + + public function delete(User $user): JsonResponse + { + Gate::authorize('delete', $user); + + (new DeleteUserAction())->execute($user); + + return $this->success(null, 201); + } +} diff --git a/backend/src/DishPlanner/User/Policies/UserPolicy.php b/backend/src/DishPlanner/User/Policies/UserPolicy.php new file mode 100644 index 0000000..6f78c07 --- /dev/null +++ b/backend/src/DishPlanner/User/Policies/UserPolicy.php @@ -0,0 +1,65 @@ +exists; + } + + /** + * Determine whether the user can view the model. + */ + public function view(Planner $planner, User $user): bool + { + return $planner->id === $user->planner_id; + } + + /** + * Determine whether the user can create models. + */ + public function create(Planner $planner): bool + { + return $this->viewAny($planner); + } + + /** + * Determine whether the user can update the model. + */ + public function update(Planner $planner, User $model): bool + { + return $this->view($planner, $model); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Planner $planner, User $model): bool + { + return $this->view($planner, $model); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Planner $planner, User $model): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(Planner $planner, User $model): bool + { + return false; + } +} diff --git a/backend/src/DishPlanner/User/Requests/CreateUserRequest.php b/backend/src/DishPlanner/User/Requests/CreateUserRequest.php new file mode 100644 index 0000000..5db139c --- /dev/null +++ b/backend/src/DishPlanner/User/Requests/CreateUserRequest.php @@ -0,0 +1,15 @@ + ['required', 'string', 'min:1', 'max:255'], + ]; + } +} diff --git a/backend/src/DishPlanner/User/Requests/UpdateUserRequest.php b/backend/src/DishPlanner/User/Requests/UpdateUserRequest.php new file mode 100644 index 0000000..a53754a --- /dev/null +++ b/backend/src/DishPlanner/User/Requests/UpdateUserRequest.php @@ -0,0 +1,15 @@ + ['required', 'string', 'min:1', 'max:255'], + ]; + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/CreateFixedRecurrenceAction.php b/backend/src/DishPlanner/UserDish/Actions/CreateFixedRecurrenceAction.php new file mode 100644 index 0000000..709e4df --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/CreateFixedRecurrenceAction.php @@ -0,0 +1,33 @@ + $value, + ]); + + $userDish->recurrences()->create([ + 'recurrence_id' => $recurrence->id, + 'recurrence_type' => $recurrence::class, + ]); + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/CreateMinimumRecurrenceAction.php b/backend/src/DishPlanner/UserDish/Actions/CreateMinimumRecurrenceAction.php new file mode 100644 index 0000000..18e0f19 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/CreateMinimumRecurrenceAction.php @@ -0,0 +1,44 @@ +recurrences + ->filter(fn (UserDishRecurrence $recurrence) => $recurrence->recurrence_type === MinimumRecurrence::class); + + if ($existingRecurrenceForDay->isNotEmpty()) { + $first = $existingRecurrenceForDay->pop(); + $existingRecurrenceForDay->each(fn (UserDishRecurrence $recurrence) => $recurrence->delete()); + + $first->recurrence->days = $recurrenceValue; + $first->recurrence->save(); + + return; + } + + $recurrence = MinimumRecurrence::create([ + 'days' => $recurrenceValue, + ]); + + $userDish->recurrences()->create([ + 'recurrence_id' => $recurrence->id, + 'recurrence_type' => $recurrenceType, + ]); + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/CreateUserDishAction.php b/backend/src/DishPlanner/UserDish/Actions/CreateUserDishAction.php new file mode 100644 index 0000000..a21f2a6 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/CreateUserDishAction.php @@ -0,0 +1,50 @@ + $user->id, + 'dish_id' => $dish->id, + ]); + + $this->addRecurrences($userDish, $data); + + return $userDish->refresh(); + } + + /** + * @throws InvalidRecurrenceTypeException + */ + private function addRecurrences(UserDish $userDish, array $data): void + { + $recurrenceType = Arr::get($data, 'recurrence_type'); + $recurrenceValue = Arr::get($data, 'recurrence_value'); + + if (is_null($recurrenceType) || is_null($recurrenceValue)) { + return; + } + + if ($recurrenceType === WeeklyRecurrence::class) { + (new CreateFixedRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue); + } elseif ($recurrenceType === MinimumRecurrence::class) { + (new CreateMinimumRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue); + } else { + throw new InvalidRecurrenceTypeException(); + } + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/DeleteFixedRecurrenceAction.php b/backend/src/DishPlanner/UserDish/Actions/DeleteFixedRecurrenceAction.php new file mode 100644 index 0000000..a9d7701 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/DeleteFixedRecurrenceAction.php @@ -0,0 +1,22 @@ +delete(); + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/DeleteMinimumRecurrenceAction.php b/backend/src/DishPlanner/UserDish/Actions/DeleteMinimumRecurrenceAction.php new file mode 100644 index 0000000..cfc8044 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/DeleteMinimumRecurrenceAction.php @@ -0,0 +1,28 @@ +where('recurrence_type', MinimumRecurrence::class) + ->where('recurrence_id', $recurrence->id) + ->delete(); + + $recurrence->delete(); + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/SyncRecurrencesForUserDishAction.php b/backend/src/DishPlanner/UserDish/Actions/SyncRecurrencesForUserDishAction.php new file mode 100644 index 0000000..2099712 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/SyncRecurrencesForUserDishAction.php @@ -0,0 +1,43 @@ +recurrences() + ->each(function (UserDishRecurrence $recurrence) { + $recurrence->recurrence->delete(); + $recurrence->delete(); + }); + + $recurrences->each(function (array $recurrence) use ($userDish) { + $recurrenceType = Arr::get($recurrence, 'type'); + $recurrenceValue = Arr::get($recurrence, 'value'); + + if (! $recurrenceType || ! $recurrenceValue) { + return; + } + + match ($recurrenceType) { + WeeklyRecurrence::class => (new CreateFixedRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue), + MinimumRecurrence::class => (new CreateMinimumRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue), + default => throw new InvalidRecurrenceTypeException(), + }; + }); + + return $userDish->refresh(); + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/UpdateFixedRecurrenceAction.php b/backend/src/DishPlanner/UserDish/Actions/UpdateFixedRecurrenceAction.php new file mode 100644 index 0000000..bd4d43b --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/UpdateFixedRecurrenceAction.php @@ -0,0 +1,32 @@ +weekday === $weekday) { + return $recurrence; + } + + $recurrence->weekday = $weekday; + $recurrence->save(); + + return $recurrence; + } +} diff --git a/backend/src/DishPlanner/UserDish/Actions/UpdateMinimumRecurrenceAction.php b/backend/src/DishPlanner/UserDish/Actions/UpdateMinimumRecurrenceAction.php new file mode 100644 index 0000000..0142dad --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Actions/UpdateMinimumRecurrenceAction.php @@ -0,0 +1,32 @@ +days === $days) { + return $recurrence; + } + + $recurrence->days = $days; + $recurrence->save(); + + return $recurrence; + } +} diff --git a/backend/src/DishPlanner/UserDish/Controllers/ListUserDishesController.php b/backend/src/DishPlanner/UserDish/Controllers/ListUserDishesController.php new file mode 100644 index 0000000..c515152 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Controllers/ListUserDishesController.php @@ -0,0 +1,26 @@ +user(); + + /** @var UserDishRepository $userDishRepository */ + $userDishRepository = resolve(UserDishRepository::class); + $userDishes = $userDishRepository->getAllForPlanner($planner); + + return $this->success([ + 'user_dishes' => UserDishResource::collection($userDishes)->collection->toArray() + ]); + } +} diff --git a/backend/src/DishPlanner/UserDish/Controllers/UserDishController.php b/backend/src/DishPlanner/UserDish/Controllers/UserDishController.php new file mode 100644 index 0000000..23eea18 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Controllers/UserDishController.php @@ -0,0 +1,64 @@ +success([ + 'user' => new UserWithUserDishesResource($user), + ]); + } + + public function show(User $user, Dish $dish): JsonResponse + { + Gate::authorize('view', $user); + Gate::authorize('view', $dish); + + $userDish = UserDish::query() + ->where('user_id', $user->id) + ->where('dish_id', $dish->id) + ->firstOrFail(); + + Gate::authorize('view', $userDish); + + return $this->success([ + 'user_dish' => new UserDishResource($userDish), + ]); + } + + /** + * @throws InvalidRecurrenceTypeException + */ + public function store(CreateUserDishRequest $request, User $user, Dish $dish): JsonResponse + { + $userDish = (new CreateUserDishAction())->execute($dish, $user, $request->validated()); + + return $this->success([ + 'user_dish' => new UserDishResource($userDish), + ]); + } + + public function destroy(User $user, Dish $dish): JsonResponse + { + (new DeleteUserDishAction())->execute($user, $dish); + + return $this->success(null); + } +} diff --git a/backend/src/DishPlanner/UserDish/Controllers/UserDishRecurrenceController.php b/backend/src/DishPlanner/UserDish/Controllers/UserDishRecurrenceController.php new file mode 100644 index 0000000..425fba1 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Controllers/UserDishRecurrenceController.php @@ -0,0 +1,98 @@ +error('UNKNOWN_USER_DISH', 404); + } + + $recurrences = collect($request->validated()); + + (new SyncRecurrencesForUserDishAction())->execute($userDish, $recurrences); + + return $this->success([ + 'user_dish' => new UserDishResource($userDish->refresh()), + ]); + } + + /** + * @throws InvalidRecurrenceTypeException + */ + public function update(UpdateUserDishFixedRecurrenceRequest $request, UserDish $userDish, string $recurrenceType, int $recurrenceId): JsonResponse + { + $recurrenceClass = $this->getRecurrenceClass($recurrenceType); + $recurrence = $recurrenceClass::findOrFail($recurrenceId); + + if ($recurrence instanceof WeeklyRecurrence) { + (new UpdateFixedRecurrenceAction())->execute($recurrence, $request->validated()); + } elseif ($recurrenceClass === MinimumRecurrence::class) { + (new UpdateMinimumRecurrenceAction())->execute($recurrence, $request->validated()); + } else { + return $this->error('invalid recurrence type'); + } + + return $this->success([ + 'user_dish' => new UserDishResource($userDish->refresh()), + ]); + } + + /** + * @throws InvalidRecurrenceTypeException + */ + public function destroy(UserDish $userDish, string $recurrenceType, int $recurrenceId): JsonResponse + { + $recurrenceClass = $this->getRecurrenceClass($recurrenceType); + $recurrence = $recurrenceClass::findOrFail($recurrenceId); + + if ($recurrence instanceof WeeklyRecurrence) { + (new DeleteFixedRecurrenceAction())->execute($recurrence); + } elseif ($recurrenceClass === MinimumRecurrence::class) { + (new DeleteMinimumRecurrenceAction())->execute($recurrence); + } else { + return $this->error('invalid recurrence type'); + } + + return $this->success([ + 'user_dish' => new UserDishResource($userDish->refresh()), + ]); + } + + /** + * @throws InvalidRecurrenceTypeException + */ + private function getRecurrenceClass(string $recurrenceType): string + { + return match ($recurrenceType) { + 'fixed' => WeeklyRecurrence::class, + 'minimum' => MinimumRecurrence::class, + default => throw new InvalidRecurrenceTypeException("Invalid recurrence type: $recurrenceType") + }; + } +} diff --git a/backend/src/DishPlanner/UserDish/Exceptions/InvalidRecurrenceTypeException.php b/backend/src/DishPlanner/UserDish/Exceptions/InvalidRecurrenceTypeException.php new file mode 100644 index 0000000..e528e3f --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Exceptions/InvalidRecurrenceTypeException.php @@ -0,0 +1,10 @@ +id === $userDish->dish?->planner_id && $planner->id === $userDish->user?->planner_id; + } + + /** + * Determine whether the user can create models. + */ + public function create(Planner $planner): bool + { + return false; + } + + /** + * Determine whether the user can update the model. + */ + public function update(Planner $planner, UserDish $plannerDish): bool + { + return false; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Planner $planner, UserDish $plannerDish): bool + { + return $this->view($planner, $plannerDish); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Planner $planner, UserDish $plannerDish): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(Planner $planner, UserDish $plannerDish): bool + { + return false; + } + + /** + * Assign the userDish to a schedule + */ + public function assign(Planner $planner, UserDish $userDish): bool + { + return $planner->id === $userDish->dish?->planner_id && $planner->id === $userDish->user?->planner_id; + } +} diff --git a/backend/src/DishPlanner/UserDish/Repositories/UserDishRepository.php b/backend/src/DishPlanner/UserDish/Repositories/UserDishRepository.php new file mode 100644 index 0000000..789c335 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Repositories/UserDishRepository.php @@ -0,0 +1,104 @@ +findCandidatesForDate($user, $date)->random(); + } + + public function findCandidatesForDate(User $user, Carbon $date): SupportCollection + { + /** @var UserDishRepository $userDishRepository */ + $userDishRepository = resolve(UserDishRepository::class); + $userDish = $userDishRepository->findForUserByFixedRecurrenceOnDay($user, $date->dayOfWeek); + + if ($userDish) { + return collect([$userDish]); + } + + $interferingUserDishes = $userDishRepository->findInterferingUserDishes($user, $date); + + return $user->userDishes + ->reject(fn (UserDish $userDish) => $interferingUserDishes + ->filter(fn (UserDish $ud) => $ud->id === $userDish->id) + ->isNotEmpty() + ); + } + + public function findForUserByFixedRecurrenceOnDay(User $user, int $dayOfWeek): ?UserDish + { + return UserDish::query() + ->where('user_id', $user->id) + ->whereHas('recurrences', fn ($query) => $query + ->where('recurrence_type', WeeklyRecurrence::class) + ->whereHasMorph( + 'recurrence', + [WeeklyRecurrence::class], + fn ($query) => $query->where('weekday', $dayOfWeek) + ) + ) + ->first(); + } + + // Interfering: their minimum overlaps the date + public function findInterferingUserDishes(User $user, Carbon $date): Collection + { + // get maximum interval number + $maxInterval = MinimumRecurrence::query()->max('days'); + + // get whole schedule for now - maxInterval <> now + maxInterval + return new Collection( + Schedule::query() + ->whereBetween('date', [$date->copy()->subDays($maxInterval), $date->copy()->addDays($maxInterval)]) + ->get() + ->flatMap(fn (Schedule $schedule) => $schedule->scheduledUserDishes) + ->filter(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish->user_id === $user->id) + ->filter(fn(ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish->recurrences->contains('recurrence_type', MinimumRecurrence::class)) + ->filter(function (ScheduledUserDish $scheduledUserDish) use ($date) { + $minimum = $scheduledUserDish->userDish + ->recurrences + ->firstWhere('recurrence_type', MinimumRecurrence::class) + ->recurrence + ->days; + $dateRange = CarbonPeriod::create($scheduledUserDish->schedule->date->subDays($minimum), $scheduledUserDish->schedule->date->addDays($minimum)); + + return $dateRange->contains($date); + }) + ->map(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish) + ); + } + + public static function getForUserAndDish(User $user, Dish $dish): ?UserDish + { + return UserDish::query() + ->where('user_id', $user->id) + ->where('dish_id', $dish->id) + ->first(); + } + + public function getAllForPlanner(Planner $planner): Collection + { + return UserDish::query() + ->with(['user', 'dish']) + ->whereHas('user', fn ($query) => $query->where('planner_id', $planner->id)) + ->whereHas('dish', fn ($query) => $query->where('planner_id', $planner->id)) + ->get(); + } +} diff --git a/backend/src/DishPlanner/UserDish/Requests/CreateUserDishRequest.php b/backend/src/DishPlanner/UserDish/Requests/CreateUserDishRequest.php new file mode 100755 index 0000000..e6c5bd7 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Requests/CreateUserDishRequest.php @@ -0,0 +1,22 @@ + ['sometimes', Rule::in([ + WeeklyRecurrence::class, + MinimumRecurrence::class, + ])], + 'recurrence_value' => ['required_with:recurrence_type', 'integer'], + ]; + } +} diff --git a/backend/src/DishPlanner/UserDish/Requests/StoreUserDishRecurrenceRequest.php b/backend/src/DishPlanner/UserDish/Requests/StoreUserDishRecurrenceRequest.php new file mode 100644 index 0000000..2c50204 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Requests/StoreUserDishRecurrenceRequest.php @@ -0,0 +1,27 @@ + [ + 'sometimes', + 'string', + Rule::in([ + MinimumRecurrence::class, + WeeklyRecurrence::class, + ]), + 'required_with:*.recurrence_value' + ], + '*.value' => ['sometimes', 'integer', 'required_with:*.recurrence_type'], + ]; + } +} diff --git a/backend/src/DishPlanner/UserDish/Requests/UpdateUserDishFixedRecurrenceRequest.php b/backend/src/DishPlanner/UserDish/Requests/UpdateUserDishFixedRecurrenceRequest.php new file mode 100644 index 0000000..49b23f9 --- /dev/null +++ b/backend/src/DishPlanner/UserDish/Requests/UpdateUserDishFixedRecurrenceRequest.php @@ -0,0 +1,35 @@ + [ + 'required', + 'string', + 'in:' . implode(',', [ + MinimumRecurrence::class, + WeeklyRecurrence::class, + ]), + ], + 'recurrence_data' => 'required|array', + 'recurrence_data.days' => 'required_if:recurrence_type,' . MinimumRecurrence::class . '|integer|min:1', + 'recurrence_data.weekday' => 'required_if:recurrence_type,' . WeeklyRecurrence::class . '|integer|between:0,6', + ]; + } + + public function messages(): array + { + return [ + 'recurrence_data.days.required_if' => 'The days field is required for minimum recurrence.', + 'recurrence_data.weekday.required_if' => 'The weekday field is required for weekly recurrence.', + ]; + } +} diff --git a/backend/storage/app/.gitignore b/backend/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/backend/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/backend/storage/app/private/.gitignore b/backend/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/app/public/.gitignore b/backend/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/.gitignore b/backend/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/backend/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/backend/storage/framework/cache/.gitignore b/backend/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/backend/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/backend/storage/framework/cache/data/.gitignore b/backend/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/sessions/.gitignore b/backend/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/testing/.gitignore b/backend/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/views/.gitignore b/backend/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/logs/.gitignore b/backend/storage/logs/.gitignore new file mode 100755 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/tailwind.config.js b/backend/tailwind.config.js new file mode 100644 index 0000000..ce0c57f --- /dev/null +++ b/backend/tailwind.config.js @@ -0,0 +1,20 @@ +import defaultTheme from 'tailwindcss/defaultTheme'; + +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', + './storage/framework/views/*.php', + './resources/**/*.blade.php', + './resources/**/*.js', + './resources/**/*.vue', + ], + theme: { + extend: { + fontFamily: { + sans: ['Figtree', ...defaultTheme.fontFamily.sans], + }, + }, + }, + plugins: [], +}; diff --git a/backend/tests/Feature/Dish/AddUsersToDishTest.php b/backend/tests/Feature/Dish/AddUsersToDishTest.php new file mode 100755 index 0000000..2c65a7a --- /dev/null +++ b/backend/tests/Feature/Dish/AddUsersToDishTest.php @@ -0,0 +1,88 @@ +planner; + $users = User::factory()->planner($planner)->count($userCount)->create(); + $dish = Dish::factory()->planner($planner)->create(); + + $this->assertDatabaseEmpty(UserDish::class); + + $this + ->actingAs($planner) + ->post(route('api.dishes.users.add', [ + 'dish' => $dish, + ]), [ + 'users' => $users->map->id->toArray(), + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('dish', fn ($json) => $json + ->has('id') + ->where('planner_id', $planner->id) + ->where('name', $dish->name) + ->has('users', $userCount) + ->where('users', $users + ->map(fn (User $user) => [ + 'id' => $user->id, + 'name' => $user->name, + ])->toArray() + ) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(UserDish::class, $userCount); + + $dish->refresh(); + $this->assertEquals($userCount, $dish->users->count()); + } + + public function test_it_does_not_sync_users_to_a_user_dish_from_another_planner(): void + { + $userCount = 4; + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $users = User::factory()->planner($otherPlanner)->count($userCount)->create(); + $dish = Dish::factory()->planner($otherPlanner)->create(); + + $this->assertDatabaseEmpty(UserDish::class); + + $this + ->actingAs($planner) + ->post(route('api.dishes.users.add', [ + 'dish' => $dish, + ]), [ + 'users' => $users->map->id->toArray(), + ]) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ['MODEL_NOT_FOUND']) + ); + + $this->assertDatabaseCount(UserDish::class, 0); + $this->assertEquals(0, $dish->refresh()->users->count()); + } +} diff --git a/backend/tests/Feature/Dish/CreateDishTest.php b/backend/tests/Feature/Dish/CreateDishTest.php new file mode 100755 index 0000000..1f551e1 --- /dev/null +++ b/backend/tests/Feature/Dish/CreateDishTest.php @@ -0,0 +1,89 @@ +planner; + + $this->assertDatabaseEmpty(Dish::class); + $this->assertDatabaseEmpty('user_dishes'); + + $this + ->actingAs($planner) + ->post(route('api.dishes.store'), [ + 'name' => 'Pizza', + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('dish', fn ($json) => $json + ->has('id') + ->where('planner_id', $planner->id) + ->where('name', 'Pizza') + ->has('users') + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(Dish::class, 1); + + $dish = Dish::all()->first(); + $this->assertEquals($planner->id, $dish->planner_id); + } + + #[DataProvider('invalidNameValues')] + public function test_it_throws_exception_for_invalid_name_values(?string $name, string $expectedError): void + { + $planner = $this->planner; + + $this + ->actingAs($planner) + ->post(route('api.dishes.store'), $name ? ['name' => $name] : []) +// ->assertStatus(422) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', [$expectedError]) + ); + + } + + public static function invalidNameValues(): array + { + return [ + 'empty name' => [ + 'name' => '', + 'expectedError' => 'The name field is required.', + ], + 'null name' => [ + 'name' => null, + 'expectedError' => 'The name field is required.', + ], + 'too long name' => [ + 'name' => Str::random(130), + 'expectedError' => 'The name field must not be greater than 128 characters.', + ], + 'too short name' => [ + 'name' => 'a', + 'expectedError' => 'The name field must be at least 3 characters.', + ], + ]; + } +} diff --git a/backend/tests/Feature/Dish/DeleteDishTest.php b/backend/tests/Feature/Dish/DeleteDishTest.php new file mode 100644 index 0000000..efbcd54 --- /dev/null +++ b/backend/tests/Feature/Dish/DeleteDishTest.php @@ -0,0 +1,82 @@ +planner; + $dish = Dish::factory()->planner($planner)->create(); + + $this->assertDatabaseCount(Dish::class, 1); + + $this + ->actingAs($planner) + ->delete(route('api.dishes.destroy', $dish)) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseEmpty(Dish::class); + } + + public function test_it_deletes_user_dishes_when_deleting_a_dish(): void + { + $planner = $this->planner; + $dish = Dish::factory()->planner($planner)->create(); + $user = User::factory()->planner($planner)->create(); + $user->dishes()->attach($dish); + + $this->assertDatabaseCount(UserDish::class, 1); + + $this + ->actingAs($planner) + ->delete(route('api.dishes.destroy', $dish)) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseEmpty(UserDish::class); + } + + + public function test_planner_cannot_delete_dish_from_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $dish = Dish::factory()->planner($otherPlanner)->create(); + + $this->assertDatabaseCount(Dish::class, 1); + + $this + ->actingAs($planner) + ->delete(route('api.dishes.destroy', $dish)) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ['MODEL_NOT_FOUND']) + ); + + $this->assertDatabaseCount(Dish::class, 1); + } +} diff --git a/backend/tests/Feature/Dish/ListDishesTest.php b/backend/tests/Feature/Dish/ListDishesTest.php new file mode 100755 index 0000000..0f2bff9 --- /dev/null +++ b/backend/tests/Feature/Dish/ListDishesTest.php @@ -0,0 +1,61 @@ +planner; + $dishes = Dish::factory()->planner($planner)->count(rand(2, 10))->create(); + + $this + ->actingAs($planner) + ->get(route('api.dishes.index')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->where('dishes', $dishes + ->map(fn (Dish $dish) => [ + 'id' => $dish->id, + 'planner_id' => $planner->id, + 'name' => $dish->name, + 'users' => $dish->users->pluck('id')->toArray(), + ]) + ->toArray() + ) + ) + ->where('errors', null) + ); + } + + public function test_user_cannot_see_list_of_other_users_dishes(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + Dish::factory()->planner($otherPlanner)->count(rand(2, 10))->create(); + + $this + ->actingAs($planner) + ->get(route('api.dishes.index')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->where('dishes', []) + ) + ->where('errors', null) + ); + } +} diff --git a/backend/tests/Feature/Dish/RemoveUsersFromDishTest.php b/backend/tests/Feature/Dish/RemoveUsersFromDishTest.php new file mode 100755 index 0000000..d300140 --- /dev/null +++ b/backend/tests/Feature/Dish/RemoveUsersFromDishTest.php @@ -0,0 +1,94 @@ +planner; + $users = User::factory()->planner($planner)->count($userCount)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $dish->users()->attach($users); + + $removedUser = $users->random(); + $remainingUsers = $users->reject(fn (User $user) => $user->id === $removedUser->id)->values(); + + $this->assertDatabaseCount(UserDish::class, $userCount); + + $this + ->actingAs($planner) + ->post(route('api.dishes.users.remove', [ + 'dish' => $dish, + ]), [ + 'users' => [$removedUser->id], + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('dish', fn ($json) => $json + ->has('id') + ->where('planner_id', $planner->id) + ->where('name', $dish->name) + ->has('users', $remainingUsers->count()) + ->where('users', $remainingUsers + ->map(fn (User $user) => [ + 'id' => $user->id, + 'name' => $user->name, + ])->toArray() + ) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(UserDish::class, $userCount - 1); + + $dish->refresh(); + $this->assertEquals($userCount - 1, $dish->users->count()); + } + + public function test_it_does_not_sync_users_to_a_user_dish_of_another_planner(): void + { + $userCount = 4; + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $users = User::factory()->planner($otherPlanner)->count($userCount)->create(); + $dish = Dish::factory()->planner($otherPlanner)->create(); + $dish->users()->attach($users); + + $removedUser = $users->random(); + + $this->assertDatabaseCount(UserDish::class, $userCount); + + $this + ->actingAs($planner) + ->post(route('api.dishes.users.remove', [ + 'dish' => $dish, + ]), [ + 'users' => [$removedUser->id], + ]) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ['MODEL_NOT_FOUND']) + ); + + $this->assertDatabaseCount(UserDish::class, $userCount); + } +} diff --git a/backend/tests/Feature/Dish/ShowDishTest.php b/backend/tests/Feature/Dish/ShowDishTest.php new file mode 100755 index 0000000..8990f35 --- /dev/null +++ b/backend/tests/Feature/Dish/ShowDishTest.php @@ -0,0 +1,51 @@ +planner; + $dish = Dish::factory()->planner($planner)->create(); + + $this + ->actingAs($planner) + ->get(route('api.dishes.show', $dish)) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('dish', fn ($json) => $json + ->where('id', $dish->id) + ->where('planner_id', $planner->id) + ->where('name', $dish->name) + ->has('users') + ) + ) + ->where('errors', null) + ); + } + + public function test_user_cannot_see_dish_from_other_user(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $dish = Dish::factory()->planner($otherPlanner)->create(); + + $this + ->actingAs($planner) + ->get(route('api.dishes.show', $dish)) + ->assertStatus(404); + } +} diff --git a/backend/tests/Feature/Dish/SyncUsersForDishTest.php b/backend/tests/Feature/Dish/SyncUsersForDishTest.php new file mode 100755 index 0000000..69b0224 --- /dev/null +++ b/backend/tests/Feature/Dish/SyncUsersForDishTest.php @@ -0,0 +1,88 @@ +planner; + $users = User::factory()->planner($planner)->count($userCount)->create(); + $dish = Dish::factory()->planner($planner)->create(); + + $this->assertDatabaseEmpty(UserDish::class); + + $this + ->actingAs($planner) + ->post(route('api.dishes.users.sync', [ + 'dish' => $dish, + ]), [ + 'users' => $users->map->id->toArray(), + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('dish', fn ($json) => $json + ->has('id') + ->where('planner_id', $planner->id) + ->where('name', $dish->name) + ->has('users', $userCount) + ->where('users', $users + ->map(fn (User $user) => [ + 'id' => $user->id, + 'name' => $user->name, + ])->toArray() + ) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(UserDish::class, $userCount); + + $dish->refresh(); + $this->assertEquals($userCount, $dish->users->count()); + } + + public function test_it_does_not_sync_users_to_a_user_dish_of_other_planner(): void + { + $userCount = 4; + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $users = User::factory()->planner($otherPlanner)->count($userCount)->create(); + $dish = Dish::factory()->planner($otherPlanner)->create(); + + $this->assertDatabaseEmpty(UserDish::class); + + $this + ->actingAs($planner) + ->post(route('api.dishes.users.sync', [ + 'dish' => $dish, + ]), [ + 'users' => $users->map->id->toArray(), + ]) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ['MODEL_NOT_FOUND']) + ); + + $this->assertDatabaseEmpty(UserDish::class); + $this->assertEmpty($dish->refresh()->users->count()); + } +} diff --git a/backend/tests/Feature/Dish/UpdateDishTest.php b/backend/tests/Feature/Dish/UpdateDishTest.php new file mode 100755 index 0000000..907d03e --- /dev/null +++ b/backend/tests/Feature/Dish/UpdateDishTest.php @@ -0,0 +1,80 @@ +planner; + $nameOriginal = 'Pizza'; + $nameUpdated = 'Lasagne'; + + $dish = Dish::factory() + ->planner($planner) + ->create([ + 'name' => $nameOriginal, + ]); + + $this + ->actingAs($planner) + ->put(route('api.dishes.update', $dish->id), [ + 'name' => $nameUpdated, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('dish', fn ($json) => $json + ->where('id', $dish->id) + ->where('planner_id', $planner->id) + ->where('name', $nameUpdated) + ->has('users') + ) + ) + ->where('errors', null) + ); + + $dish->refresh(); + $this->assertEquals($nameUpdated, $dish->name); + } + + public function test_user_cannot_update_other_users_dish(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $nameOriginal = 'Pizza'; + $nameUpdated = 'Lasagne'; + + $dish = Dish::factory() + ->planner($otherPlanner) + ->create([ + 'name' => $nameOriginal, + ]); + + $this + ->actingAs($planner) + ->put(route('api.dishes.update', $dish->id), [ + 'name' => $nameUpdated, + ]) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ['MODEL_NOT_FOUND']) + ); + + $dish->refresh(); + $this->assertEquals($nameOriginal, $dish->name); + } +} diff --git a/backend/tests/Feature/PlannerLoginTest.php b/backend/tests/Feature/PlannerLoginTest.php new file mode 100644 index 0000000..eccf3a5 --- /dev/null +++ b/backend/tests/Feature/PlannerLoginTest.php @@ -0,0 +1,98 @@ +create([ + 'email' => 'planner@example.com', + 'password' => Hash::make('secret123'), + ]); + + $response = $this + ->actingAs($planner) + ->post(route('api.auth.login'), [ + 'email' => 'planner@example.com', + 'password' => 'secret123', + ]); + + $response->assertOk(); + $this->assertAuthenticatedAs($planner); + } + + public function test_login_fails_with_invalid_credentials(): void + { + Planner::factory()->create([ + 'email' => 'planner@example.com', + 'password' => Hash::make('secret123'), + ]); + + $response = $this->postJson(route('api.auth.login'), [ + 'email' => 'planner@example.com', + 'password' => 'wrongpassword', + ]); + + $response->assertUnauthorized(); + } + + public function test_a_logged_in_planner_can_log_out(): void + { + $planner = Planner::factory()->create([ + 'password' => Hash::make('secret123'), + ]); + + $this->post(route('api.auth.login'), [ + 'email' => $planner->email, + 'password' => 'secret123', + ]); + + $response = $this->post(route('api.auth.logout')); + + $response->assertOk(); + $this->assertGuest(); // nobody should be logged in after logout + } + + public function test_planner_can_register(): void + { + $schedulesCount = Schedule::all()->count(); + + $response = $this->post(route('api.auth.register'), [ + 'name' => 'High Functioning Planner', + 'email' => 'planner@example.com', + 'password' => 'secret123', + 'password_confirmation' => 'secret123', + ]); + + $response->assertCreated(); + + $this->assertDatabaseHas('planners', [ + 'email' => 'planner@example.com', + ]); + + $this->assertGreaterThan($schedulesCount, Schedule::all()->count()); + } + + public function test_it_returns_the_authenticated_planner(): void + { + $planner = Planner::factory()->create(); + + $this + ->actingAs($planner) + ->get(route('api.auth.me')) + ->assertOk() + ->assertJsonFragment([ + 'email' => $planner->email, + 'name' => $planner->name, + ]); + } +} diff --git a/backend/tests/Feature/Schedule/GenerateScheduleTest.php b/backend/tests/Feature/Schedule/GenerateScheduleTest.php new file mode 100644 index 0000000..7ed1888 --- /dev/null +++ b/backend/tests/Feature/Schedule/GenerateScheduleTest.php @@ -0,0 +1,257 @@ +planner; + $users = User::factory()->planner($planner)->count(2)->create(); // Create users + $dishes = Dish::factory()->planner($planner)->count(200)->create(); // Create dishes + + // Attach dishes to users, ensuring `UserDish` records exist for every user + $users->each(function (User $user) use ($dishes) { + $dishes->random(50)->each(function (Dish $dish) use ($user) { + UserDish::factory()->create([ + 'user_id' => $user->id, + 'dish_id' => $dish->id, + ]); + }); + }); + + $this->assertDatabaseEmpty(Schedule::class); + + $this + ->actingAs($planner) + ->post(route('api.schedule.generate')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseCount(Schedule::class, 14); + } + + public function test_fresh_schedule_adheres_to_fixed_recurrences(): void + { + $planner = $this->planner; + $targetDate = now()->addDay(); + $users = User::factory()->planner($planner)->count(2)->create(); // Create users + $dishes = Dish::factory()->planner($planner)->count(200)->create(); // Create dishes + + // Attach dishes to users, ensuring `UserDish` records exist for every user + $users->each(function (User $user) use ($dishes) { + $dishes->random(50)->each(function (Dish $dish) use ($user) { + UserDish::factory()->create([ + 'user_id' => $user->id, + 'dish_id' => $dish->id, + ]); + }); + }); + + $targetUserDish = UserDish::query() + ->whereNotNull('dish_id') + ->whereNotNull('user_id') + ->get() + ->random(); + + $weeklyRecurrence = WeeklyRecurrence::factory()->create([ + 'weekday' => $targetDate->dayOfWeek(), + ]); + + $fixedRecurrence = UserDishRecurrence::factory()->create([ + 'user_dish_id' => $targetUserDish->id, + 'recurrence_id' => $weeklyRecurrence->id, + 'recurrence_type' => $weeklyRecurrence::class, + ]); + + $this->assertDatabaseEmpty(Schedule::class); + + $this + ->actingAs($planner) + ->post(route('api.schedule.generate')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseCount(Schedule::class, 14); + $targetDateSchedule = Schedule::query()->where('date', $targetDate->format('Y-m-d'))->first(); + + $this->assertNotNull($targetDateSchedule); + + $targetScheduledUserDishes = $targetDateSchedule + ->scheduledUserDishes + ->pluck('user_dish_id') + ->toArray(); + + $this->assertContains($targetUserDish->id, $targetScheduledUserDishes); + } + public function test_schedule_can_be_overwritten(): void + { + $planner = $this->planner; + $users = User::factory()->planner($planner)->count(2)->create(); // Create users + $dishes = Dish::factory()->planner($planner)->count(200)->create(); // Create dishes + + // Attach dishes to users, ensuring `UserDish` records exist for every user + $users->each(function (User $user) use ($dishes) { + $dishes->random(50)->each(function (Dish $dish) use ($user) { + UserDish::factory()->create([ + 'user_id' => $user->id, + 'dish_id' => $dish->id, + ]); + }); + }); + + // Assert that every user has `UserDish` records + $users->each(fn (User $user) => + $this->assertNotEmpty($user->refresh()->userDishes) + ); + + $scheduleDay = Schedule::factory() + ->planner($planner) + ->create([ + 'date' => Carbon::now()->addDay(), + ]); + + $users->each(fn (User $user) => ScheduledUserDish::factory() + ->create([ + 'user_dish_id' => $user->userDishes->random()->id, + 'schedule_id' => $scheduleDay->id, + 'user_id' => $user->id, + ]) + ); + + $scheduleDay->refresh(); + $originalUserDishes = $scheduleDay->scheduledUserDishes->map(fn (ScheduledUserDish $scheduledUserDish) => [ + 'user_dish_id' => $scheduledUserDish->user_dish_id, + ]); + + $this->assertDatabaseCount(Schedule::class, 1); + $this->assertDatabaseCount(ScheduledUserDish::class, 2); + + $this + ->actingAs($planner) + ->post(route('api.schedule.generate'), [ + 'overwrite' => true, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseCount(Schedule::class, 14); + + $freshScheduleDay = Schedule::query()->where('date', $scheduleDay->date)->first(); + $this->assertNotEquals( + $originalUserDishes, + $freshScheduleDay->scheduledUserDishes->map(fn (ScheduledUserDish $scheduledUserDish) => [ + 'user_dish_id' => $scheduledUserDish->user_dish_id, + ]) + ); + } + + public function test_fixed_recurrence_takes_precedence_during_overwrite(): void + { + $targetDate = now()->addDay(); + $planner = $this->planner; + $users = User::factory()->planner($planner)->count(2)->create(); // Create users + $dishes = Dish::factory()->planner($planner)->count(200)->create(); // Create dishes + + // Attach dishes to users, ensuring `UserDish` records exist for every user + $users->each(function (User $user) use ($dishes) { + $dishes->random(50)->each(function (Dish $dish) use ($user) { + UserDish::factory()->create([ + 'user_id' => $user->id, + 'dish_id' => $dish->id, + ]); + }); + }); + + $scheduleDay = Schedule::factory() + ->planner($planner) + ->date($targetDate) + ->create(); + + $fixedUser = $users->pop(); + $targetUserDish = $fixedUser->userDishes->random(); + + $weeklyRecurrence = WeeklyRecurrence::factory()->create([ + 'weekday' => $targetDate->dayOfWeek, + ]); + + UserDishRecurrence::factory()->create([ + 'user_dish_id' => $targetUserDish->id, + 'recurrence_id' => $weeklyRecurrence->id, + 'recurrence_type' => $weeklyRecurrence::class, + ]); + + $users + ->map(fn (User $user) => ScheduledUserDish::factory() + ->create([ + 'schedule_id' => $scheduleDay->id, + 'user_dish_id' => $user->userDishes->random()->id, + 'user_id' => $user->id, + ])); + + ScheduledUserDish::factory() + ->schedule($scheduleDay) + ->userDish($targetUserDish) + ->create(); + + $scheduleDay->refresh(); + + // assert all days have been filled + $this->assertDatabaseCount(Schedule::class, 1); + $this->assertDatabaseCount(ScheduledUserDish::class, 2); + + + $this + ->actingAs($planner) + ->post(route('api.schedule.generate'), [ + 'overwrite' => true, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseCount(Schedule::class, 14); + + $targetDateSchedule = Schedule::query()->where('date', $targetDate->format('Y-m-d'))->first(); + $this->assertNotNull($targetDateSchedule); + + $targetScheduledUserDishes = $targetDateSchedule + ->scheduledUserDishes + ->filter(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish->user->id === $fixedUser->id); + + $this->assertCount(1, $targetScheduledUserDishes); + $this->assertContains($targetUserDish->id, $targetScheduledUserDishes->pluck('user_dish_id')->toArray()); + } +} diff --git a/backend/tests/Feature/Schedule/ListScheduleTest.php b/backend/tests/Feature/Schedule/ListScheduleTest.php new file mode 100644 index 0000000..be3dc3d --- /dev/null +++ b/backend/tests/Feature/Schedule/ListScheduleTest.php @@ -0,0 +1,97 @@ +planner; + $expectedDateRangeStart = '2024-01-03'; + $expectedDateRangeEnd = '2024-01-05'; + + $users = User::factory()->planner($planner)->count(10)->create(); + $dishes = Dish::factory()->planner($planner)->count(10)->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->attach($users)); + + $period = CarbonPeriod::create($expectedDateRangeStart, $expectedDateRangeEnd); + foreach ($period as $date) { + $schedule = Schedule::factory()->planner($planner)->date($date)->create(); + $users->each(function (User $user) use ($schedule) { + $randomUserDish = $user->userDishes->random(); + return $schedule->scheduledUserDishes()->create([ + 'user_dish_id' => $randomUserDish->id, + 'user_id' => $randomUserDish->user->id, + ]); + }); + } + + $this + ->actingAs($planner) + ->get(url()->query(route('api.schedule.index'), [ + 'start' => $expectedDateRangeStart, + 'end' => $expectedDateRangeEnd, + ])) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('schedule', Schedule::query() + ->whereBetween('date', [$expectedDateRangeStart, $expectedDateRangeEnd]) + ->count() + ) + ) + ->where('errors', null) + ); + } + + public function test_it_does_not_show_dishes_of_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $expectedDateRangeStart = '2024-01-03'; + $expectedDateRangeEnd = '2024-01-05'; + + $users = User::factory()->planner($otherPlanner)->count(10)->create(); + $dishes = Dish::factory()->planner($otherPlanner)->count(10)->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->attach($users)); + + $period = CarbonPeriod::create($expectedDateRangeStart, $expectedDateRangeEnd); + foreach ($period as $date) { + $schedule = Schedule::factory()->planner($otherPlanner)->date($date)->create(); + $users->each(function (User $user) use ($schedule) { + $randomUserDish = $user->userDishes->random(); + return $schedule->scheduledUserDishes()->create([ + 'user_dish_id' => $randomUserDish->id, + 'user_id' => $randomUserDish->user->id, + ]); + }); + } + + $this + ->actingAs($planner) + ->get(url()->query(route('api.schedule.index'), [ + 'start' => $expectedDateRangeStart, + 'end' => $expectedDateRangeEnd, + ])) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload.schedule', []) + ->where('errors', null) + ); + } +} diff --git a/backend/tests/Feature/Schedule/ReadScheduleTest.php b/backend/tests/Feature/Schedule/ReadScheduleTest.php new file mode 100644 index 0000000..3c473b6 --- /dev/null +++ b/backend/tests/Feature/Schedule/ReadScheduleTest.php @@ -0,0 +1,111 @@ +planner; + $userOne = User::factory()->planner($planner)->create(['name' => 'Melissa']); + $userTwo = User::factory()->planner($planner)->create(['name' => 'Jochen']); + + $users = collect([$userOne, $userTwo]); + $dishes = Dish::factory()->planner($planner)->count(rand(15, 20))->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->sync($users->pluck('id')->toArray())); + + $dateRangeStart = '2024-01-01'; + $dateRangeEnd = '2024-01-07'; + + $period = CarbonPeriod::create($dateRangeStart, $dateRangeEnd); + + foreach ($period as $date) { + $schedule = Schedule::factory()->planner($planner)->date($date)->create(); + + $users->each(fn (User $user) => ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($user->userDishes->random()) + ->create() + ); + } + + $randomSchedule = $planner->schedules->random(); + + $this + ->actingAs($planner) + ->get(route('api.schedule.show', $randomSchedule->date->format('Y-m-d'))) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('schedule', fn (AssertableJson $json) => $json + ->where('id', $randomSchedule->id) + ->where('is_skipped', false) + ->where('date', $randomSchedule->date->format('Y-m-d')) + ->has('scheduled_user_dishes', 2) + ) + ) + ->where('errors', null) + ); + } + + public function test_single_day_cannot_be_read_by_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $userOne = User::factory()->planner($otherPlanner)->create(['name' => 'Melissa']); + $userTwo = User::factory()->planner($otherPlanner)->create(['name' => 'Jochen']); + + $users = collect([$userOne, $userTwo]); + $dishes = Dish::factory()->planner($otherPlanner)->count(rand(15, 20))->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->sync($users->pluck('id')->toArray())); + + $dateRangeStart = '2024-01-01'; + $dateRangeEnd = '2024-01-07'; + + $period = CarbonPeriod::create($dateRangeStart, $dateRangeEnd); + + foreach ($period as $date) { + $schedule = Schedule::factory()->planner($otherPlanner)->date($date)->create(); + + $users->each(fn (User $user) => ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($user->userDishes->random()) + ->create() + ); + } + + $this->assertEmpty($planner->schedules); + + $randomDateFromPeriod = collect($period)->random()->format('Y-m-d'); + + $this + ->actingAs($planner) + ->get(route('api.schedule.show', $randomDateFromPeriod)) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload.schedule', fn (AssertableJson $json) => $json + ->has('id') + ->where('date', $randomDateFromPeriod) + ->where('is_skipped', null) + ->where('scheduled_user_dishes', []) + ) + ->where('errors', null) + ); + } +} diff --git a/backend/tests/Feature/Schedule/ScheduleUserDishTest.php b/backend/tests/Feature/Schedule/ScheduleUserDishTest.php new file mode 100644 index 0000000..8c3fd83 --- /dev/null +++ b/backend/tests/Feature/Schedule/ScheduleUserDishTest.php @@ -0,0 +1,100 @@ +create(); + + $date = Carbon::tomorrow()->startOfDay(); + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $userDish = UserDish::factory()->user($user)->dish($dish)->create(); + $schedule = Schedule::factory()->planner($planner)->date($date)->create(); + + $this + ->actingAs($planner) + ->post( + uri: route('api.schedule.user-dish.update', $schedule->date->format('Y-m-d')), + data: [ + 'user_dish_id' => $userDish->id, + 'user_id' => $user->id, + ] + ) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn (AssertableJson $json) => $json + ->where('schedule.id', $schedule->id) + ->where('schedule.scheduled_user_dishes.0.user_dish.id', $userDish->id) + ->where('schedule.scheduled_user_dishes.0.user_dish.dish.id', $dish->id) + ->etc() + ) + ->where('errors', null) + ); + } + + public function test_skipping_for_single_user(): void + { + $planner = Planner::factory()->create(); + + $this->assertDatabaseEmpty(ScheduledUserDish::class); + + $date = Carbon::tomorrow()->startOfDay(); + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $userDish = UserDish::factory()->user($user)->dish($dish)->create(); + $schedule = Schedule::factory()->planner($planner)->date($date)->create(); + ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($userDish) + ->user($user) + ->create(); + + $this->assertDatabaseCount(ScheduledUserDish::class, 1); + + $this + ->actingAs($planner) + ->post( + uri: route('api.schedule.user-dish.update', $schedule->date->format('Y-m-d')), + data: [ + 'user_dish_id' => null, + 'user_id' => $user->id, + 'skipped' => true, + ] + ) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn (AssertableJson $json) => $json + ->where('schedule.id', $schedule->id) + ->has('schedule.scheduled_user_dishes', 1) + ->has('schedule.scheduled_user_dishes.0', fn (AssertableJson $json) => $json + ->whereNull('user_dish') + ->where('user.id', $user->id) + ->where('skipped', true) + ->etc() + ) + ->etc() + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(ScheduledUserDish::class, 1); + } +} diff --git a/backend/tests/Feature/Schedule/UpdateScheduleTest.php b/backend/tests/Feature/Schedule/UpdateScheduleTest.php new file mode 100644 index 0000000..ab10c86 --- /dev/null +++ b/backend/tests/Feature/Schedule/UpdateScheduleTest.php @@ -0,0 +1,66 @@ +planner; + $userOne = User::factory()->planner($planner)->create(); + $userTwo = User::factory()->planner($planner)->create(); + $users = collect([$userOne, $userTwo]); + $schedule = Schedule::factory()->planner($planner)->create(); + $dishes = Dish::factory()->planner($planner)->count(20)->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->attach($users)); + ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($dishes->random()->userDishes->random()) + ->create([]); + ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($dishes->random()->userDishes->random()) + ->create([]); + + $schedule->refresh(); + + $this->assertFalse($schedule->is_skipped); + $this->assertCount(2, $schedule->scheduledUserDishes); + $schedule->scheduledUserDishes->each(fn ($scheduledUserDish) => $this->assertNotNull($scheduledUserDish->user_dish_id)); + + $this + ->actingAs($planner) + ->put(route('api.schedule.update', $schedule->date->format('Y-m-d')), [ + 'is_skipped' => true, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('schedule', fn (AssertableJson $json) => $json + ->where('id', $schedule->id) + ->where('date', $schedule->date->format('Y-m-d')) + ->where('is_skipped', true) + ->etc() + ) + ) + ->where('errors', null) + ); + + $schedule->refresh(); + $this->assertTrue($schedule->is_skipped); + $schedule->scheduledUserDishes->each(fn ($scheduledUserDish) => $this->assertNull($scheduledUserDish->dish_id)); + } +} diff --git a/backend/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php new file mode 100644 index 0000000..3acf60b --- /dev/null +++ b/backend/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php @@ -0,0 +1,121 @@ +planner; + $userOne = User::factory()->planner($planner)->create(); + $userTwo = User::factory()->planner($planner)->create(); + $users = collect([$userOne, $userTwo]); + + $dish = Dish::factory()->planner($planner)->create(); + $dish->users()->attach($users); + $scheduleDate = '2025-12-13'; + + $targetUserDish = $dish->userDishes->random(); + + $this->assertDatabaseEmpty(Schedule::class); + $this->assertDatabaseEmpty(ScheduledUserDish::class); + + $this + ->actingAs($planner) + ->post(route('api.scheduled-user-dishes.store'), [ + 'user_dish_id' => $targetUserDish->id, + 'date' => $scheduleDate, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('scheduled_user_dish', fn ($json) => $json + ->has('id') + ->where('is_skipped', null) + ->has('schedule', fn (AssertableJson $json) => $json + ->has('id') + ->where('date', $scheduleDate) + ) + ->has('userDish', fn (AssertableJson $json) => $json + ->where('id', $targetUserDish->id) + ->has('user', fn (AssertableJson $json) => $json + ->where('id', $targetUserDish->user->id) + ->where('name', $targetUserDish->user->name) + ) + ->has('dish', fn (AssertableJson $json) => $json + ->where('id', $targetUserDish->dish->id) + ->where('planner_id', $targetUserDish->dish->planner_id) + ->where('name', $targetUserDish->dish->name) + ->has('users', 2) + ) + ->has('recurrences') + ) + ) + ) + ->where('errors', null) + ); + + + $this->assertDatabaseCount(Schedule::class, 1); + $this->assertDatabaseHas(Schedule::class, [ + 'date' => $scheduleDate, + ]); + + $this->assertDatabaseCount(ScheduledUserDish::class, 1); + $this->assertDatabaseHas(ScheduledUserDish::class, [ + 'user_dish_id' => $targetUserDish->id, + 'schedule_id' => Schedule::all()->first()->id, + ]); + } + + public function test_planner_cannot_schedule_user_dishes_from_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $userOne = User::factory()->planner($otherPlanner)->create(); + $userTwo = User::factory()->planner($otherPlanner)->create(); + $users = collect([$userOne, $userTwo]); + + $dish = Dish::factory()->planner($otherPlanner)->create(); + $dish->users()->attach($users); + $scheduleDate = '2025-12-13'; + + $targetUserDish = $dish->userDishes->random(); + + $this->assertDatabaseEmpty(Schedule::class); + $this->assertDatabaseEmpty(ScheduledUserDish::class); + + $this + ->actingAs($planner) + ->post(route('api.scheduled-user-dishes.store'), [ + 'user_dish_id' => $targetUserDish->id, + 'date' => $scheduleDate, + ]) + ->assertStatus(403) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->whereNull('payload') + ->where('errors', [ + "This action is unauthorized." + ]) + ); + + + $this->assertDatabaseEmpty(Schedule::class); + $this->assertDatabaseEmpty(ScheduledUserDish::class); + } +} diff --git a/backend/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php new file mode 100755 index 0000000..22b0486 --- /dev/null +++ b/backend/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php @@ -0,0 +1,82 @@ +planner; + $userOne = User::factory()->planner($planner)->create(); + $userTwo = User::factory()->planner($planner)->create(); + $users = collect([$userOne, $userTwo]); + + $dish = Dish::factory()->planner($planner)->create(); + $dish->users()->attach($users); + $schedule = Schedule::factory()->planner($planner)->create(); + $scheduledUserDish = ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($dish->userDishes->random()) + ->create(); + + $this->assertDatabaseCount(ScheduledUserDish::class, 1); + + $this + ->actingAs($planner) + ->delete(route('api.scheduled-user-dishes.destroy', $scheduledUserDish)) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseEmpty(ScheduledUserDish::class); + } + + public function test_planner_cannot_delete_a_scheduled_dish_of_another_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $userOne = User::factory()->planner($otherPlanner)->create(); + $userTwo = User::factory()->planner($otherPlanner)->create(); + $users = collect([$userOne, $userTwo]); + + $dish = Dish::factory()->planner($otherPlanner)->create(); + $dish->users()->attach($users); + $schedule = Schedule::factory()->planner($otherPlanner)->create(); + $scheduledUserDish = ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($dish->userDishes->random()) + ->create(); + + $this->assertDatabaseCount(ScheduledUserDish::class, 1); + + $this + ->actingAs($planner) + ->delete(route('api.scheduled-user-dishes.destroy', $scheduledUserDish)) + ->assertStatus(403) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', [ + "This action is unauthorized." + ]) + ); + + $this->assertDatabaseCount(ScheduledUserDish::class, 1); + } +} diff --git a/backend/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php new file mode 100644 index 0000000..8c44ec5 --- /dev/null +++ b/backend/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php @@ -0,0 +1,130 @@ +planner; + $userOne = User::factory()->planner($planner)->create(['name' => 'Melissa']); + $userTwo = User::factory()->planner($planner)->create(['name' => 'Jochen']); + + $users = collect([$userOne, $userTwo]); + $dishes = Dish::factory()->planner($planner)->count(rand(15, 20))->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->sync($users->pluck('id')->toArray())); + + $dateRangeStart = '2024-01-01'; + $dateRangeEnd = '2024-01-07'; + + $period = CarbonPeriod::create($dateRangeStart, $dateRangeEnd); + + foreach ($period as $date) { + $schedule = Schedule::factory()->planner($planner)->date($date)->create(); + + $users->each(function (User $user) use ($schedule) { + $randomUserDish = $user->userDishes->random(); + + return ScheduledUserDish::factory() + ->schedule($schedule) + ->user($randomUserDish->user) + ->userDish($randomUserDish) + ->create(); + }); + } + + $randomScheduledUserDish = ScheduledUserDish::all()->random(); + + $this + ->actingAs($planner) + ->get(route('api.scheduled-user-dishes.show', $randomScheduledUserDish)) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('scheduled_user_dish', fn (AssertableJson $json) => $json + ->where('id', $randomScheduledUserDish->id) + ->where('is_skipped', false) + ->has('schedule', fn (AssertableJson $json) => $json + ->has('id') + ->where('date', $randomScheduledUserDish->schedule->date->format('Y-m-d')) + ) + ->has('userDish', fn (AssertableJson $json) => $json + ->has('id') + ->has('user', fn (AssertableJson $json) => $json + ->where('id', $randomScheduledUserDish->userDish->user->id) + ->where('name', $randomScheduledUserDish->userDish->user->name) + ) + ->has('dish', fn (AssertableJson $json) => $json + ->where('id', $randomScheduledUserDish->userDish->dish->id) + ->where('planner_id', $planner->id) + ->where('name', $randomScheduledUserDish->userDish->dish->name) + ->has('users', 2) + ) + ->has('recurrences') + ) + ) + ) + ->where('errors', null) + ); + } + + public function test_planner_cannot_read_scheduled_user_dish_from_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $userOne = User::factory()->planner($otherPlanner)->create(['name' => 'Melissa']); + $userTwo = User::factory()->planner($otherPlanner)->create(['name' => 'Jochen']); + + $users = collect([$userOne, $userTwo]); + $dishes = Dish::factory()->planner($otherPlanner)->count(rand(15, 20))->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->sync($users->pluck('id')->toArray())); + + $dateRangeStart = '2024-01-01'; + $dateRangeEnd = '2024-01-07'; + + $period = CarbonPeriod::create($dateRangeStart, $dateRangeEnd); + + foreach ($period as $date) { + $schedule = Schedule::factory()->planner($otherPlanner)->date($date)->create(); + + $users->each(function (User $user) use ($schedule) { + $randomUserDish = $user->userDishes->random(); + + return ScheduledUserDish::factory() + ->schedule($schedule) + ->user($randomUserDish->user) + ->userDish($randomUserDish) + ->create(); + }); + } + + $randomScheduledUserDish = ScheduledUserDish::all()->random(); + + $this + ->actingAs($planner) + ->get(route('api.scheduled-user-dishes.show', $randomScheduledUserDish)) + ->assertStatus(403) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', [ + "This action is unauthorized." + ]) + ); + } +} diff --git a/backend/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php new file mode 100644 index 0000000..580055a --- /dev/null +++ b/backend/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php @@ -0,0 +1,172 @@ +planner; + $userOne = User::factory()->planner($planner)->create(); + $userTwo = User::factory()->planner($planner)->create(); + $users = collect([$userOne, $userTwo]); + + $this->generateDishes($planner); + $this->generateScheduledDishes($planner); + + $dishOne = Dish::factory()->planner($planner)->create(); + $dishOne->users()->sync($users); + $dishTwo = Dish::factory()->planner($planner)->create(); + $dishTwo->users()->sync($users); + + $targetUser = $users->pop(); + $oldUserDish = $targetUser->userDishes()->inRandomOrder()->first(); + $newUserDish = $dishTwo->userDishes()->inRandomOrder()->first(); + + $schedule = Schedule::factory()->planner($planner)->create(); + $randomScheduledUserDish = ScheduledUserDish::factory() + ->schedule($schedule) + ->user($oldUserDish->user) + ->userDish($oldUserDish) + ->create(); + + $this + ->actingAs($planner) + ->put(route('api.scheduled-user-dishes.update', $randomScheduledUserDish), [ + 'user_dish_id' => $newUserDish->id, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('scheduled_user_dish', fn (AssertableJson $json) => $json + ->where('id', $randomScheduledUserDish->id) + ->where('is_skipped', false) + ->has('schedule', fn (AssertableJson $json) => $json + ->has('id') + ->where('date', $randomScheduledUserDish->schedule->date->format('Y-m-d')) + ) + ->has('userDish', fn (AssertableJson $json) => $json + ->where('id', $newUserDish->id) + ->has('user', fn (AssertableJson $json) => $json + ->where('id', $newUserDish->user->id) + ->where('name', $newUserDish->user->name) + ) + ->has('dish', fn (AssertableJson $json) => $json + ->where('id', $newUserDish->dish->id) + ->where('planner_id', $planner->id) + ->where('name', $newUserDish->dish->name) + ->has('users', 2) + ) + ->has('recurrences') + ) + ) + ) + ->where('errors', null) + ); + } + + public function test_planner_cannot_update_dish_of_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $userOne = User::factory()->planner($otherPlanner)->create(); + $userTwo = User::factory()->planner($otherPlanner)->create(); + $users = collect([$userOne, $userTwo]); + + $this->generateDishes($otherPlanner); + $this->generateScheduledDishes($otherPlanner); + + $dishOne = Dish::factory()->planner($otherPlanner)->create(); + $dishOne->users()->sync($users); + $dishTwo = Dish::factory()->planner($otherPlanner)->create(); + $dishTwo->users()->sync($users); + + $targetUser = $users->pop(); + $oldUserDish = $targetUser->userDishes()->inRandomOrder()->first(); + $newUserDish = $dishTwo->userDishes()->inRandomOrder()->first(); + + $schedule = Schedule::factory()->planner($otherPlanner)->create(); + $randomScheduledUserDish = ScheduledUserDish::factory() + ->schedule($schedule) + ->user($oldUserDish->user) + ->userDish($oldUserDish) + ->create(); + + $this + ->actingAs($planner) + ->put(route('api.scheduled-user-dishes.update', $randomScheduledUserDish), [ + 'user_dish_id' => $newUserDish->id, + ]) + ->assertStatus(403) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', [ + "This action is unauthorized." + ]) + ); + } + + public function test_is_skipped_update_succeeds(): void + { + $planner = $this->planner; + $userOne = User::factory()->planner($planner)->create(); + $userTwo = User::factory()->planner($planner)->create(); + $users = collect([$userOne, $userTwo]); + + $this->generateDishes($planner); + $this->generateScheduledDishes($planner); + + $dishOne = Dish::factory()->planner($planner)->create(); + $dishOne->users()->sync($users); + $dishTwo = Dish::factory()->planner($planner)->create(); + $dishTwo->users()->sync($users); + $schedule = Schedule::factory()->planner($planner)->create(); + $randomUser = $users->random(); + $randomScheduledUserDish = ScheduledUserDish::factory() + ->schedule($schedule) + ->user($randomUser) + ->userDish($randomUser->userDishes->random()) + ->create(); + + $this + ->actingAs($planner) + ->put(route('api.scheduled-user-dishes.update', $randomScheduledUserDish), [ + 'is_skipped' => true, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('scheduled_user_dish', fn (AssertableJson $json) => $json + ->where('id', $randomScheduledUserDish->id) + ->where('userDish', null) + ->where('is_skipped', true) + ->has('schedule', fn (AssertableJson $json) => $json + ->has('id') + ->where('date', $randomScheduledUserDish->schedule->date->format('Y-m-d')) + ) + ) + ) + ->where('errors', null) + ); + } +} diff --git a/backend/tests/Feature/User/CreateUserTest.php b/backend/tests/Feature/User/CreateUserTest.php new file mode 100644 index 0000000..3f3ce11 --- /dev/null +++ b/backend/tests/Feature/User/CreateUserTest.php @@ -0,0 +1,41 @@ +planner; + $newUserName = fake()->name; + + $this->assertDatabaseEmpty(User::class); + + $this + ->actingAs($planner) + ->post(route('api.users.create'), [ + 'name' => $newUserName, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user', fn ($json) => $json + ->has('id') + ->where('name', $newUserName) + ) + ) + ->where('errors', null) + ); + } +} diff --git a/backend/tests/Feature/User/DeleteUserTest.php b/backend/tests/Feature/User/DeleteUserTest.php new file mode 100644 index 0000000..3526de9 --- /dev/null +++ b/backend/tests/Feature/User/DeleteUserTest.php @@ -0,0 +1,52 @@ +planner; + $user = User::factory()->planner($planner)->create(); + + $this + ->actingAs($planner) + ->delete(route('api.users.delete', $user)) + ->assertStatus(201) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + } + + public function test_planner_cannot_update_user_of_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $newUserName = fake()->name; + $user = User::factory()->planner($otherPlanner)->create(); + + $this + ->actingAs($planner) + ->put(route('api.users.update', $user), [ + 'name' => $newUserName, + ]) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ["MODEL_NOT_FOUND"]) + ); + } +} diff --git a/backend/tests/Feature/User/Dish/ListUserDishesTest.php b/backend/tests/Feature/User/Dish/ListUserDishesTest.php new file mode 100644 index 0000000..5b12ab4 --- /dev/null +++ b/backend/tests/Feature/User/Dish/ListUserDishesTest.php @@ -0,0 +1,60 @@ +planner; + $users = User::factory()->planner($planner)->count(10)->create(); + $dishes = Dish::factory()->planner($planner)->count(10)->create(); + $users->each(function ($user) use ($dishes) { + $user->dishes()->attach($dishes->random(5)); + }); + + $this + ->actingAs($planner) + ->get(route('api.user-dishes.index')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload.user_dishes', 10 * 5) + ->where('errors', null) + ); + } + + public function test_planner_cannot_see_user_dishes_from_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $users = User::factory()->planner($planner)->count(10)->create(); + $dishes = Dish::factory()->planner($planner)->count(10)->create(); + $users->each(function ($user) use ($dishes) { + $user->dishes()->attach($dishes->random(rand(1, 5))); + }); + + $this + ->actingAs($otherPlanner) + ->get(route('api.user-dishes.index')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload.user_dishes', 0) + ->where('errors', null) + ); + } + +} diff --git a/backend/tests/Feature/User/Dish/RemoveDishesForUserTest.php b/backend/tests/Feature/User/Dish/RemoveDishesForUserTest.php new file mode 100755 index 0000000..a04ced3 --- /dev/null +++ b/backend/tests/Feature/User/Dish/RemoveDishesForUserTest.php @@ -0,0 +1,47 @@ +assertDatabaseEmpty(UserDish::class); + + $planner = $this->planner; + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $user->dishes()->attach($dish); + + $this->assertDatabaseCount(UserDish::class, 1); + + $this + ->actingAs($planner) + ->delete(route('api.users.dishes.destroy', [ + 'dish' => $dish, + 'user' => $user + ]), []) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload', null) + ->where('errors', null) + ); + + $this->assertDatabaseEmpty(UserDish::class); + + $user->refresh(); + $this->assertEmpty($user->dishes); + } +} diff --git a/backend/tests/Feature/User/Dish/ShowUserDishTest.php b/backend/tests/Feature/User/Dish/ShowUserDishTest.php new file mode 100644 index 0000000..02cdb5b --- /dev/null +++ b/backend/tests/Feature/User/Dish/ShowUserDishTest.php @@ -0,0 +1,78 @@ +planner; + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $user->dishes()->attach($dish); + + $userDish = $user->userDishes->first(); + + $this + ->actingAs($planner) + ->get(route('api.users.dishes.show', [ + 'user' => $user->id, + 'dish' => $dish->id, + ])) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user_dish', fn ($json) => $json + ->where('id', $userDish->id) + ->has('user', fn ($json) => $json + ->where('id', $user->id) + ->where('name', $user->name) + ) + ->has('dish', fn ($json) => $json + ->where('id', $dish->id) + ->where('planner_id', $planner->id) + ->where('name', $dish->name) + ->has('users') + ) + ->where('recurrences', []) + ) + ) + ->where('errors', null) + ); + } + + public function test_planner_cannot_see_user_dish_of_other_user(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $user = User::factory()->planner($otherPlanner)->create(); + $dish = Dish::factory()->planner($otherPlanner)->create(); + $user->dishes()->attach($dish); + + $this + ->actingAs($planner) + ->get(route('api.users.dishes.show', [ + 'user' => $user->id, + 'dish' => $dish->id, + ])) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ['MODEL_NOT_FOUND']) + ); + } +} diff --git a/backend/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php b/backend/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php new file mode 100755 index 0000000..ec04b68 --- /dev/null +++ b/backend/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php @@ -0,0 +1,272 @@ +planner; + $recurrenceType = WeeklyRecurrence::class; + $recurrenceValue = WeekdaysEnum::Thursday->value; + + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $user->dishes()->attach($dish); + + $this + ->actingAs($planner) + ->post(route('api.users.dishes.recurrences.store', [ + 'dish' => $dish, + 'user' => $user, + ]), [ + [ + 'type' => $recurrenceType, + 'value' => $recurrenceValue, + ], + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user_dish', fn ($json) => $json + ->has('id') + ->where('dish.name', $dish->name) + ->has('user', fn ($json) => $json + ->where('id', $user->id) + ->where('name', $user->name) + ) + ->has('recurrences', 1) + ->has('recurrences.0', fn ($json) => $json + ->has('id') + ->where('type', $recurrenceType) + ->where('value', $recurrenceValue) + ) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(WeeklyRecurrence::class, 1); + } + + public function test_it_adds_minimum_recurrence_to_user_dish(): void + { + $planner = $this->planner; + $recurrenceType = MinimumRecurrence::class; + $recurrenceValue = 5; + + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $user->dishes()->attach($dish); + + $this + ->actingAs($planner) + ->post(route('api.users.dishes.recurrences.store', [ + 'dish' => $dish, + 'user' => $user + ]), [ + [ + 'type' => $recurrenceType, + 'value' => $recurrenceValue, + ] + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user_dish', fn ($json) => $json + ->has('id') + ->where('dish.id', $dish->id) + ->where('dish.planner_id', $dish->planner_id) + ->where('dish.name', $dish->name) + ->where('user.id', $user->id) + ->where('user.name', $user->name) + ->has('recurrences', 1) + ->has('recurrences.0', fn ($json) => $json + ->has('id') + ->where('type', $recurrenceType) + ->where('value', $recurrenceValue) + ) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(MinimumRecurrence::class, 1); + } + + public function test_it_adds_multiple_recurrences_to_user_dish(): void + { + $planner = $this->planner; + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $user->dishes()->attach($dish); + + $this + ->actingAs($planner) + ->post(route('api.users.dishes.recurrences.store', [ + 'dish' => $dish, + 'user' => $user + ]), [ + [ + 'type' => MinimumRecurrence::class, + 'value' => 5, + ], + [ + 'type' => WeeklyRecurrence::class, + 'value' => WeekdaysEnum::Thursday->value, + ], + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user_dish', fn ($json) => $json + ->has('id') + ->where('dish.id', $dish->id) + ->where('dish.planner_id', $dish->planner_id) + ->where('dish.name', $dish->name) + ->where('user.id', $user->id) + ->where('user.name', $user->name) + ->has('recurrences', 2) + ->has('recurrences.0', fn ($json) => $json + ->has('id') + ->where('type', MinimumRecurrence::class) + ->where('value', 5) + ) + ->has('recurrences.1', fn ($json) => $json + ->has('id') + ->where('type', WeeklyRecurrence::class) + ->where('value', WeekdaysEnum::Thursday->value) + ) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(MinimumRecurrence::class, 1); + $this->assertDatabaseHas(MinimumRecurrence::class, [ + 'days' => 5, + ]); + $this->assertDatabaseCount(WeeklyRecurrence::class, 1); + $this->assertDatabaseHas(WeeklyRecurrence::class, [ + 'weekday' => WeekdaysEnum::Thursday->value, + ]); + } + + public function test_it_removes_all_recurrences(): void + { + $planner = $this->planner; + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $user->dishes()->attach($dish); + + $this + ->actingAs($planner) + ->post(route('api.users.dishes.recurrences.store', [ + 'dish' => $dish, + 'user' => $user + ]), []) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user_dish', fn ($json) => $json + ->has('id') + ->where('dish.id', $dish->id) + ->where('dish.planner_id', $dish->planner_id) + ->where('dish.name', $dish->name) + ->where('user.id', $user->id) + ->where('user.name', $user->name) + ->where('recurrences', []) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseEmpty(MinimumRecurrence::class); + $this->assertDatabaseEmpty(WeeklyRecurrence::class); + } + + public function test_it_removes_other_recurrences_to_user_dish(): void + { + $planner = $this->planner; + MinimumRecurrence::query()->truncate(); + WeeklyRecurrence::query()->truncate(); + UserDishRecurrence::query()->truncate(); + + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + + $userDish = UserDish::factory()->create([ + 'user_id' => $user->id, + 'dish_id' => $dish->id, + ]); + + $recurrence = MinimumRecurrence::factory()->create([ + 'days' => 5, + ]); + + UserDishRecurrence::factory()->create([ + 'user_dish_id' => $userDish->id, + 'recurrence_id' => $recurrence->id, + 'recurrence_type' => MinimumRecurrence::class, + ]); + + $this->assertDatabaseCount(MinimumRecurrence::class, 1); + + $this + ->actingAs($planner) + ->post(route('api.users.dishes.recurrences.store', [ + 'dish' => $dish, + 'user' => $user + ]), [ + [ + 'type' => WeeklyRecurrence::class, + 'value' => WeekdaysEnum::Thursday->value, + ], + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user_dish', fn ($json) => $json + ->has('id') + ->where('dish.id', $dish->id) + ->where('dish.name', $dish->name) + ->where('user.id', $user->id) + ->where('user.name', $user->name) + ->has('recurrences', 1) + ->has('recurrences.0', fn ($json) => $json + ->has('id') + ->where('type', WeeklyRecurrence::class) + ->where('value', WeekdaysEnum::Thursday->value) + ) + ) + ) + ->where('errors', null) + ); + + $this->assertDatabaseCount(MinimumRecurrence::class, 0); + $this->assertDatabaseCount(WeeklyRecurrence::class, 1); + $this->assertDatabaseHas(WeeklyRecurrence::class, [ + 'weekday' => WeekdaysEnum::Thursday->value, + ]); + } +} diff --git a/backend/tests/Feature/User/ListUsersTest.php b/backend/tests/Feature/User/ListUsersTest.php new file mode 100644 index 0000000..c2b694d --- /dev/null +++ b/backend/tests/Feature/User/ListUsersTest.php @@ -0,0 +1,73 @@ +planner; + $dishes = Dish::factory()->planner($planner)->count(rand(80, 100))->create(); + $users = User::factory()->planner($planner)->count(rand(2, 10))->create(); + $users->each(fn (User $user) => UserDish::factory() + ->user($user) + ->dish($dishes->random()) + ->create()); + + $this + ->actingAs($planner) + ->get(route('api.users.index')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->where('users', $users + ->sortBy('id') + ->map(fn (User $user) => [ + 'id' => $user->id, + 'name' => $user->name, + 'user_dishes' => $user->userDishes->map(fn (UserDish $userDish) => [ + 'id' => $userDish->id, + 'dish' => [ + 'id' => $userDish->dish->id, + 'name' => $userDish->dish->name, + ], + 'recurrences' => [], + ])->toArray(), + ]) + ->toArray() + ) + ) + ->where('errors', null) + ); + } + + public function test_user_cannot_see_list_of_users_of_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + User::factory()->planner($otherPlanner)->count(rand(2, 10))->create(); + + $this + ->actingAs($planner) + ->get(route('api.users.index')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->where('payload.users', []) + ->where('errors', null) + ); + } +} diff --git a/backend/tests/Feature/User/ShowUserTest.php b/backend/tests/Feature/User/ShowUserTest.php new file mode 100644 index 0000000..acc9f32 --- /dev/null +++ b/backend/tests/Feature/User/ShowUserTest.php @@ -0,0 +1,54 @@ +planner; + $user = User::factory()->planner($planner)->create(); + + $this + ->actingAs($planner) + ->get(route('api.users.show', ['user' => $user])) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user', fn ($json) => $json + ->where('id', $user->id) + ->where('name', $user->name) + ) + ) + ->where('errors', null) + ); + } + + public function test_planner_cannot_see_user_from_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $user = User::factory()->planner($otherPlanner)->create(); + + $this + ->actingAs($planner) + ->get(route('api.users.show', ['user' => $user])) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ['MODEL_NOT_FOUND']) + ); + } +} diff --git a/backend/tests/Feature/User/ShowUserWithDishesTest.php b/backend/tests/Feature/User/ShowUserWithDishesTest.php new file mode 100644 index 0000000..09a9b67 --- /dev/null +++ b/backend/tests/Feature/User/ShowUserWithDishesTest.php @@ -0,0 +1,59 @@ +planner; + $users = User::factory()->planner($planner)->count(rand(3, 10))->create(); + $dishes = Dish::factory()->planner($planner)->count(rand(3, 10))->create(); + $dishes->each(function (Dish $dish) use ($users) { + $dish->users()->sync($users->random(rand(1, 3))->pluck('id')); + }); + + $targetUser = $users->random(); + $targetUser->refresh(); + + $this + ->actingAs($planner) + ->get(route('api.users.dishes.index', [ + 'user' => $targetUser->id, + ])) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user', fn ($json) => $json + ->where('id', $targetUser->id) + ->where('name', $targetUser->name) + ->has('user_dishes', $targetUser->dishes->count()) + ->where('user_dishes', $targetUser->userDishes + ->map(fn (UserDish $userDish) => [ + 'id' => $userDish->id, + 'dish' => [ + 'id' => $userDish->dish->id, + 'name' => $userDish->dish->name, + ], + 'recurrences' => [], + ]) + ->toArray() + ) + ) + ) + ->where('errors', null) + ); + } +} diff --git a/backend/tests/Feature/User/UpdateUserTest.php b/backend/tests/Feature/User/UpdateUserTest.php new file mode 100644 index 0000000..429a240 --- /dev/null +++ b/backend/tests/Feature/User/UpdateUserTest.php @@ -0,0 +1,60 @@ +planner; + $newUserName = fake()->name; + $user = User::factory()->planner($planner)->create(); + + $this + ->actingAs($planner) + ->put(route('api.users.update', $user), [ + 'name' => $newUserName, + ]) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', true) + ->has('payload', fn ($json) => $json + ->has('user', fn ($json) => $json + ->where('id', $user->id) + ->where('name', $newUserName) + ) + ) + ->where('errors', null) + ); + } + + public function test_planner_cannot_update_user_of_other_planner(): void + { + $planner = $this->planner; + $otherPlanner = Planner::factory()->create(); + $newUserName = fake()->name; + $user = User::factory()->planner($otherPlanner)->create(); + + $this + ->actingAs($planner) + ->put(route('api.users.update', $user), [ + 'name' => $newUserName, + ]) + ->assertStatus(404) + ->assertJson(fn (AssertableJson $json) => $json + ->where('success', false) + ->where('payload', null) + ->where('errors', ["MODEL_NOT_FOUND"]) + ); + } +} diff --git a/backend/tests/TestCase.php b/backend/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/backend/tests/TestCase.php @@ -0,0 +1,10 @@ +planner($planner)->count($count)->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->sync($users->pluck('id')->toArray())); + + return $dishes; + } +} diff --git a/backend/tests/Traits/HasPlanner.php b/backend/tests/Traits/HasPlanner.php new file mode 100644 index 0000000..995e214 --- /dev/null +++ b/backend/tests/Traits/HasPlanner.php @@ -0,0 +1,20 @@ +create(); + + $this->planner = $planner; + } + +} diff --git a/backend/tests/Traits/ScheduledDishesTestTrait.php b/backend/tests/Traits/ScheduledDishesTestTrait.php new file mode 100644 index 0000000..0690289 --- /dev/null +++ b/backend/tests/Traits/ScheduledDishesTestTrait.php @@ -0,0 +1,40 @@ +generateDishes($planner); + + if (is_null($period)) { + $dateRangeStart = now(); + $dateRangeEnd = now()->addWeeks(2); + + $period = CarbonPeriod::create($dateRangeStart, $dateRangeEnd); + } + + collect($period) + ->each(function ($date) use ($users, $planner) { + $schedule = Schedule::factory()->planner($planner)->date($date)->create(); + + $users + ->each(fn (User $user) => ScheduledUserDish::factory() + ->schedule($schedule) + ->user($user) + ->userDish($user->userDishes->random()) + ->create() + ); + }); + } +} diff --git a/backend/tests/Unit/Actions/RegenerateScheduleDayActionTest.php b/backend/tests/Unit/Actions/RegenerateScheduleDayActionTest.php new file mode 100644 index 0000000..20e8f66 --- /dev/null +++ b/backend/tests/Unit/Actions/RegenerateScheduleDayActionTest.php @@ -0,0 +1,36 @@ +planner; + User::factory()->planner($planner)->count(10)->create(); + $this->generateDishes($planner); + + $schedule = Schedule::factory()->planner($planner)->create(); + + $this->assertEmpty($schedule->scheduledUserDishes); + + $mockAction = $this->mock(RegenerateScheduleDayForUserAction::class); + $mockAction->shouldReceive('execute')->times(10); + + + resolve(RegenerateScheduleDayAction::class)->execute($planner, $schedule, true); + } +} diff --git a/backend/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php b/backend/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php new file mode 100644 index 0000000..4d1f670 --- /dev/null +++ b/backend/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php @@ -0,0 +1,103 @@ +planner; + $dishes = Dish::factory()->planner($planner)->count(30)->create(); + $schedule = Schedule::factory()->planner($planner)->date($date)->create(); + $user = User::factory()->planner($planner)->create(); + $user->dishes()->attach($dishes); + + $this->assertEmpty($schedule->scheduledUserDishes); + + + resolve(RegenerateScheduleDayForUserAction::class)->execute($planner, $schedule, $user, true); + + + $expectedSchedule = Schedule::where('date', $date->format('Y-m-d'))->first(); + $this->assertCount(1, $expectedSchedule->scheduledUserDishes); + + $scheduledUserDish = ScheduledUserDish::all()->first(); + $this->assertNotNull($scheduledUserDish); + $this->assertNotNull($scheduledUserDish->userDish->id); + } + + public function test_it_updates_if_overwrite_is_true(): void + { + $planner = $this->planner; + /** @var Collection $dishes */ + $dishes = Dish::factory()->planner($planner)->count(30)->create(); + $schedule = Schedule::factory()->planner($planner)->create(); + $user = User::factory()->planner($planner)->create(); + $user->dishes()->attach($dishes); + + $startingDish = $user->userDishes->random(); + + ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($startingDish) + ->create(); + + $this->assertCount(1, $schedule->refresh()->scheduledUserDishes); + + + resolve(RegenerateScheduleDayForUserAction::class)->execute($planner, $schedule, $user, true); + + + $schedule->refresh(); + $this->assertCount(1, $schedule->scheduledUserDishes); + + $scheduledUserDish = ScheduledUserDish::all()->first(); + $this->assertNotNull($scheduledUserDish); + $this->assertNotNull($scheduledUserDish->userDish); + $this->assertNotEquals($startingDish->id, $scheduledUserDish->userDish->id); // TODO Flaky test + } + + public function test_it_does_not_update_if_overwrite_is_false(): void + { + $planner = $this->planner; + /** @var Collection $dishes */ + $dishes = Dish::factory()->planner($planner)->count(30)->create(); + $schedule = Schedule::factory()->planner($planner)->create(); + $user = User::factory()->planner($planner)->create(); + $user->dishes()->attach($dishes); + + $startingUserDish = $user->userDishes->random(); + + ScheduledUserDish::factory() + ->schedule($schedule) + ->userDish($startingUserDish) + ->create(); + + $this->assertCount(1, $schedule->refresh()->scheduledUserDishes); + + + resolve(RegenerateScheduleDayForUserAction::class)->execute($planner, $schedule, $user, false); + + + $schedule->refresh(); + $this->assertCount(1, $schedule->scheduledUserDishes); + + $scheduledUserDish = ScheduledUserDish::all()->first(); + $this->assertNotNull($scheduledUserDish); + $this->assertEquals($startingUserDish->id, $scheduledUserDish->userDish->id); + } +} diff --git a/backend/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php b/backend/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php new file mode 100644 index 0000000..451b590 --- /dev/null +++ b/backend/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php @@ -0,0 +1,41 @@ +planner; + $users = User::factory()->planner($planner)->count(10)->create(); + $dishes = Dish::factory()->planner($planner)->count(10)->create(); + $dishes->each(fn (Dish $dish) => $dish->users()->attach($users)); + + $expectedScheduleCount = 1; + + $this->assertDatabaseCount(Schedule::class, 0); + + $schedule = Schedule::create([ + 'planner_id' => $planner->id, + 'date' => now()->addDay() + ]); + + resolve(DraftScheduleForDateAction::class)->execute($schedule); + + + $this->assertDatabaseCount(Schedule::class, $expectedScheduleCount); + $this->assertDatabaseCount(ScheduledUserDish::class, $expectedScheduleCount * User::all()->count()); + } +} diff --git a/backend/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php b/backend/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php new file mode 100644 index 0000000..52669ed --- /dev/null +++ b/backend/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php @@ -0,0 +1,40 @@ +planner; + + $this->assertNotNull($planner); + + $expectedPeriodScheduleCount = 10; + $this->generateDishes($planner); + + $this->assertDatabaseCount(Schedule::class, 0); + + + resolve(DraftScheduleForPeriodAction::class) + ->execute($planner, CarbonPeriod::create(now()->addDay(), now()->addDays($expectedPeriodScheduleCount))); + + + $this->assertDatabaseCount(Schedule::class, $expectedPeriodScheduleCount); + $this->assertDatabaseCount(ScheduledUserDish::class, $expectedPeriodScheduleCount * User::all()->count()); + } +} diff --git a/backend/tests/Unit/Schedule/ScheduleGeneratorTest.php b/backend/tests/Unit/Schedule/ScheduleGeneratorTest.php new file mode 100644 index 0000000..bce3589 --- /dev/null +++ b/backend/tests/Unit/Schedule/ScheduleGeneratorTest.php @@ -0,0 +1,145 @@ +planner; + $user = User::factory()->planner($this->planner)->create(); + + $dishOne = Dish::factory()->planner($planner)->create(); + $dishTwo = Dish::factory()->planner($planner)->create(); + UserDish::factory()->user($user)->dish($dishOne)->create(); + UserDish::factory()->user($user)->dish($dishTwo)->create(); + + $this->assertDatabaseEmpty(Schedule::class); + + (new ScheduleGenerator())->generate($planner); + + $schedules = Schedule::all(); + $this->assertTrue($schedules->isNotEmpty()); + $this->assertCount(14, $schedules); + } + + public function test_it_takes_weekly_recurrences_into_account(): void + { + $recurringDay = now()->addDays(2); + + $planner = $this->planner; + $user = User::factory()->planner($this->planner)->create(); + $dishesPlain = Dish::factory()->planner($planner)->count(20)->create(); + $dishRecurring = Dish::factory()->planner($planner)->create(); + $weeklyRecurrence = WeeklyRecurrence::factory() + ->weekday(WeekdaysEnum::from($recurringDay->dayOfWeek())) + ->create(); + + $dishesPlain->each(fn (Dish $dish) => UserDish::factory() + ->user($user) + ->dish($dish) + ->create() + ); + + $userDishRecurring = UserDish::factory() + ->user($user) + ->dish($dishRecurring) + ->create(); + + UserDishRecurrence::factory()->create([ + 'user_dish_id' => $userDishRecurring->id, + 'recurrence_id' => $weeklyRecurrence->id, + 'recurrence_type' => WeeklyRecurrence::class, + ]); + + $userDishRecurring->refresh(); + $this->assertNotEmpty($userDishRecurring->recurrences); + + $this->assertDatabaseEmpty(Schedule::class); + + + (new ScheduleGenerator())->generate($planner); + + + $this->assertTrue(Schedule::all()->isNotEmpty()); + + $schedule = Schedule::query()->where('date', $recurringDay->format('Y-m-d'))->first(); + $this->assertNotNull($schedule); + $this->assertNotEmpty($schedule->scheduledUserDishes); + $this->assertEquals($schedule->scheduledUserDishes->first()->userDish->id, $userDishRecurring->id); + + $schedule = Schedule::query()->where('date', $recurringDay->addWeek()->format('Y-m-d'))->first(); + $this->assertNotNull($schedule); + $this->assertNotEmpty($schedule->scheduledUserDishes); + $this->assertEquals($schedule->scheduledUserDishes->first()->userDish->id, $userDishRecurring->id); + } + + public function test_it_takes_minimum_recurrences_into_account(): void + { + $recurringMinimum = 3; + + $planner = $this->planner; + $user = User::factory()->planner($this->planner)->create(); + $dishPlain = Dish::factory()->planner($planner)->create(); + $dishRecurring = Dish::factory()->planner($planner)->create(); + $minimumRecurrence = MinimumRecurrence::factory()->days($recurringMinimum)->create(); + UserDish::factory() + ->user($user) + ->dish($dishPlain) + ->create(); + $userDishRecurring = UserDish::factory() + ->user($user) + ->dish($dishRecurring) + ->create(); + + UserDishRecurrence::factory()->create([ + 'user_dish_id' => $userDishRecurring->id, + 'recurrence_id' => $minimumRecurrence->id, + 'recurrence_type' => MinimumRecurrence::class, + ]); + + $userDishRecurring->refresh(); + $this->assertNotEmpty($userDishRecurring->recurrences); + + $this->assertDatabaseEmpty(Schedule::class); + + + (new ScheduleGenerator())->generate($planner); + + + $this->assertTrue(Schedule::all()->isNotEmpty()); + + Schedule::all() + ->filter(fn (Schedule $schedule) => $schedule->scheduledUserDishes()->first()->userDish->dish->id === $dishRecurring->id) + ->map(fn (Schedule $schedule) => $schedule->date) + ->reduce(function (?Carbon $previousDate, Carbon $currentDate) use ($recurringMinimum) { + if (! is_null($previousDate)) { + $this->assertGreaterThanOrEqual( + $recurringMinimum, + $previousDate->diffInDays($currentDate), + 'Dates are not spaced properly' + ); + } + + return $currentDate; + }); + } +} diff --git a/backend/tests/Unit/ScheduleRepositoryTest.php b/backend/tests/Unit/ScheduleRepositoryTest.php new file mode 100644 index 0000000..600d2a5 --- /dev/null +++ b/backend/tests/Unit/ScheduleRepositoryTest.php @@ -0,0 +1,43 @@ +planner; + $date = Carbon::parse(fake()->date); + Schedule::factory()->planner($planner)->date($date)->create(); + + $this->assertDatabaseCount(Schedule::class, 1); + + $schedule = (new ScheduleRepository())->findOrCreate($planner, $date); + + $this->assertDatabaseCount(Schedule::class, 1); + $this->assertEquals($date, $schedule->date); + } + + public function test_find_or_create_creates_new_schedule_for_date(): void + { + $planner = $this->planner; + $date = Carbon::parse(fake()->date); + + $this->assertDatabaseEmpty(Schedule::class); + + $schedule = (new ScheduleRepository())->findOrCreate($planner, $date); + + $this->assertDatabaseCount(Schedule::class, 1); + $this->assertEquals($date, $schedule->date); + } +} diff --git a/backend/tests/Unit/UpdateScheduledUserDishActionTest.php b/backend/tests/Unit/UpdateScheduledUserDishActionTest.php new file mode 100644 index 0000000..b87f351 --- /dev/null +++ b/backend/tests/Unit/UpdateScheduledUserDishActionTest.php @@ -0,0 +1,35 @@ +create(); + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $otherDish = Dish::factory()->planner($planner)->create(); + $userDish = UserDish::factory()->user($user)->dish($dish)->create(); + $otherUserDish = UserDish::factory()->user($user)->dish($otherDish)->create(); + $schedule = Schedule::factory()->planner($planner)->create(); + $scheduledUserDish = ScheduledUserDish::factory()->schedule($schedule)->userDish($userDish)->create(); + + (new UpdateScheduledUserDishAction())->execute($scheduledUserDish, $otherUserDish); + + $scheduledUserDish->refresh(); + $this->assertEquals($otherUserDish->id, $scheduledUserDish->user_dish_id); + } +} diff --git a/backend/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php b/backend/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php new file mode 100644 index 0000000..efe5982 --- /dev/null +++ b/backend/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php @@ -0,0 +1,80 @@ +planner; + $user = User::factory()->planner($planner)->create(); + $dishRecurring = Dish::factory()->planner($planner)->create(); + $minimumRecurrence = MinimumRecurrence::factory()->days(5)->create(); + UserDish::factory()->user($user)->dish(Dish::factory()->planner($planner)->create())->create(); + $userDishRecurring = UserDish::factory()->user($user)->dish($dishRecurring)->create(); + + UserDishRecurrence::factory() + ->userDish($userDishRecurring) + ->recurrence($minimumRecurrence) + ->create(); + + $date = now()->startOfDay()->addDays(2); + + $schedule = Schedule::factory()->planner($planner)->date($date->copy()->addDays(2))->create(); + ScheduledUserDish::factory()->userDish($userDishRecurring)->schedule($schedule)->create(); + $this->actingAs($planner); + + + /** UserDishRepository $userDishRepository */ + $userDishRepository = resolve(UserDishRepository::class); + $userDishes = $userDishRepository->findInterferingUserDishes($user, $date); + + + $this->assertCount(1, $userDishes); + $this->assertEquals($userDishRecurring->id, $userDishes->first()->id); + } + + public function test_find_candidates_for_date(): void + { + $planner = $this->planner; + $user = User::factory()->planner($planner)->create(); + $dishRecurring = Dish::factory()->planner($planner)->create(); + $minimumRecurrence = MinimumRecurrence::factory()->days(5)->create(); + $userDishPlain = UserDish::factory()->user($user)->dish(Dish::factory()->planner($planner)->create())->create(); + $userDishRecurring = UserDish::factory()->user($user)->dish($dishRecurring)->create(); + + UserDishRecurrence::factory() + ->userDish($userDishRecurring) + ->recurrence($minimumRecurrence) + ->create(); + + $date = now()->startOfDay()->addDays(2); + + $schedule = Schedule::factory()->planner($planner)->date($date->copy()->addDays(2))->create(); + ScheduledUserDish::factory()->userDish($userDishRecurring)->schedule($schedule)->create(); + $this->actingAs($planner); + + + /** UserDishRepository $userDishRepository */ + $userDishRepository = resolve(UserDishRepository::class); + $userDishes = $userDishRepository->findCandidatesForDate($user, $date); + + + $this->assertEquals($userDishes->pluck('id')->toArray(), [$userDishPlain->id]); + } +} diff --git a/backend/vite.config.js b/backend/vite.config.js new file mode 100644 index 0000000..421b569 --- /dev/null +++ b/backend/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + ], +}); diff --git a/frontend b/frontend deleted file mode 160000 index 71ea7c3..0000000 --- a/frontend +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 71ea7c3fe9f304fa60627b4b3c9c3a709cfee3ea diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..9b7c041 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..207bf93 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100644 index 0000000..577ff1d --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + DishPlanner Copyright (C) 2025 myrmidex + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..5c4780a --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,87 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router. + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +### Docker Deployment + +To build and run using Docker: + +```bash +docker build -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/frontend/app/app.css b/frontend/app/app.css new file mode 100644 index 0000000..99345d8 --- /dev/null +++ b/frontend/app/app.css @@ -0,0 +1,15 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx new file mode 100644 index 0000000..9fc6636 --- /dev/null +++ b/frontend/app/root.tsx @@ -0,0 +1,75 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +import type { Route } from "./+types/root"; +import "./app.css"; + +export const links: Route.LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts new file mode 100644 index 0000000..102b402 --- /dev/null +++ b/frontend/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from "@react-router/dev/routes"; + +export default [index("routes/home.tsx")] satisfies RouteConfig; diff --git a/frontend/app/routes/home.tsx b/frontend/app/routes/home.tsx new file mode 100644 index 0000000..398e47c --- /dev/null +++ b/frontend/app/routes/home.tsx @@ -0,0 +1,13 @@ +import type { Route } from "./+types/home"; +import { Welcome } from "../welcome/welcome"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, + ]; +} + +export default function Home() { + return ; +} diff --git a/frontend/app/welcome/logo-dark.svg b/frontend/app/welcome/logo-dark.svg new file mode 100644 index 0000000..dd82028 --- /dev/null +++ b/frontend/app/welcome/logo-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/welcome/logo-light.svg b/frontend/app/welcome/logo-light.svg new file mode 100644 index 0000000..7328492 --- /dev/null +++ b/frontend/app/welcome/logo-light.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/welcome/welcome.tsx b/frontend/app/welcome/welcome.tsx new file mode 100644 index 0000000..8ac6e1d --- /dev/null +++ b/frontend/app/welcome/welcome.tsx @@ -0,0 +1,89 @@ +import logoDark from "./logo-dark.svg"; +import logoLight from "./logo-light.svg"; + +export function Welcome() { + return ( +
+
+
+
+ React Router + React Router +
+
+
+ +
+
+
+ ); +} + +const resources = [ + { + href: "https://reactrouter.com/docs", + text: "React Router Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/frontend/archive/.dockerignore b/frontend/archive/.dockerignore new file mode 100644 index 0000000..11ee758 --- /dev/null +++ b/frontend/archive/.dockerignore @@ -0,0 +1 @@ +.env.local diff --git a/frontend/archive/.env.local.example b/frontend/archive/.env.local.example new file mode 100644 index 0000000..87516ef --- /dev/null +++ b/frontend/archive/.env.local.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_API_URL=http://localhost +#NODE_ENV=production \ No newline at end of file diff --git a/frontend/archive/.env.production b/frontend/archive/.env.production new file mode 100644 index 0000000..2fbb406 --- /dev/null +++ b/frontend/archive/.env.production @@ -0,0 +1,2 @@ +#NEXT_PUBLIC_API_URL=http://192.168.178.177:9000 +#NODE_ENV=local \ No newline at end of file diff --git a/frontend/archive/.gitignore b/frontend/archive/.gitignore new file mode 100644 index 0000000..b601ed9 --- /dev/null +++ b/frontend/archive/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +/.idea +/.env.local +/package-lock.json + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/archive/README.md b/frontend/archive/README.md new file mode 100644 index 0000000..da15af8 --- /dev/null +++ b/frontend/archive/README.md @@ -0,0 +1,2 @@ +# DishPlanner Front End + diff --git a/frontend/archive/bin/update.sh b/frontend/archive/bin/update.sh new file mode 100755 index 0000000..7eec906 --- /dev/null +++ b/frontend/archive/bin/update.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +echo "🔄 Pulling latest changes..." +git pull origin main + +echo "🔨 Installing dependencies..." +npm install + +echo "🏗️ Building frontend..." +npm run build + +echo "🔁 Restarting frontend service..." +sudo systemctl restart dishplanner-frontend + +echo "✅ Update complete!" diff --git a/frontend/archive/build_and_push.sh b/frontend/archive/build_and_push.sh new file mode 100755 index 0000000..ba9f2c6 --- /dev/null +++ b/frontend/archive/build_and_push.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker build -t 192.168.178.152:50114/dishplanner-frontend:latest . +docker push 192.168.178.152:50114/dishplanner-frontend:latest \ No newline at end of file diff --git a/frontend/archive/eslint.config.mjs b/frontend/archive/eslint.config.mjs new file mode 100644 index 0000000..c85fb67 --- /dev/null +++ b/frontend/archive/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/frontend/archive/next.config.ts b/frontend/archive/next.config.ts new file mode 100644 index 0000000..536bef3 --- /dev/null +++ b/frontend/archive/next.config.ts @@ -0,0 +1,15 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + // Rewrite /api/* to backend container (from client-side) + async rewrites() { + return [ + { + source: "/api/:path*", + destination: "http://backend:80/api/:path*", // internal Docker DNS + }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/archive/package.json b/frontend/archive/package.json new file mode 100644 index 0000000..62e44db --- /dev/null +++ b/frontend/archive/package.json @@ -0,0 +1,33 @@ +{ + "name": "dish-planner", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "export": "next export" + }, + "dependencies": { + "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.2.0", + "classnames": "^2.5.1", + "luxon": "^3.5.0", + "next": "15.2.4", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/luxon": "^3.4.2", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.1.5", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/frontend/archive/postcss.config.mjs b/frontend/archive/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/frontend/archive/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/archive/public/dish-planner.webp b/frontend/archive/public/dish-planner.webp new file mode 100644 index 0000000000000000000000000000000000000000..3eaa04fc1c5bb37607ea9c50c582ca87c943f421 GIT binary patch literal 237142 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≥<;xK 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 z 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

%uU}gebY+{+v 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 vYRrF$m=j-Qkdu9eW` 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 literal 0 HcmV?d00001 diff --git a/frontend/archive/public/file.svg b/frontend/archive/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/archive/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/archive/public/globe.svg b/frontend/archive/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/archive/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/archive/public/next.svg b/frontend/archive/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/archive/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/archive/public/vercel.svg b/frontend/archive/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/archive/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/archive/public/window.svg b/frontend/archive/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/archive/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/archive/src/app/dishes/[id]/delete/page.tsx b/frontend/archive/src/app/dishes/[id]/delete/page.tsx new file mode 100644 index 0000000..95627e2 --- /dev/null +++ b/frontend/archive/src/app/dishes/[id]/delete/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import {useEffect, useState} from "react"; +import {use} from "react"; +import PageTitle from "@/components/ui/PageTitle"; +import {DishType} from "@/types/DishType"; +import {useRouter} from "next/navigation"; +import Link from "next/link"; +import Alert from "@/components/ui/Alert"; +import useRoutes from "@/hooks/useRoutes"; +import {deleteDish, fetchDish} from "@/utils/api/dishApi"; +import {UserType} from "@/types/UserType"; + +export default function EditDishPage({params}: { params: Promise<{ id: number }> }) { + const [name, setName] = useState(""); + const [recurrence, setRecurrence] = useState(0); + const [users, setUsers] = useState([]); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(true); // To handle loading state + const {id} = use(params) + const router = useRouter() + const routes = useRoutes(); + + useEffect(() => { + fetchDish(id) + .then((dish: DishType) => { + setName(dish.name); + setRecurrence(dish.recurrence); + setUsers(dish.users); + }) + .catch((err) => setError(err)) + .finally(() => setIsLoading(false)); + }, [id]); // Only run when `params.id` changes + + const submitForm = (e: React.MouseEvent) => { + e.preventDefault() + + deleteDish(id) + .then(() => router.push(routes.dish.index())) + .catch((err) => setError(err)) + } + + if (isLoading) { + return

Loading...

; + } + + return ( +
+
+ Delete Dish +
+ + { + error != '' && { error } + } + +
+
+ Are you sure you want to delete this dish? +
+
+ name: {name}
+ recurrence: {recurrence} + users: {users.map((user) => user.name).join(', ')} +
+ + +
+ No, take me back +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/archive/src/app/dishes/[id]/edit/page.tsx b/frontend/archive/src/app/dishes/[id]/edit/page.tsx new file mode 100644 index 0000000..364f9b1 --- /dev/null +++ b/frontend/archive/src/app/dishes/[id]/edit/page.tsx @@ -0,0 +1,59 @@ +"use client"; + +import {use, useCallback, useEffect, useState} from "react"; +import PageTitle from "@/components/ui/PageTitle"; +import EditDishForm from "@/components/features/dishes/EditDishForm"; +import {DishType} from "@/types/DishType"; +import Spinner from "@/components/Spinner"; +import {fetchDish} from "@/utils/api/dishApi"; +import SyncUsersForm from "@/components/features/dishes/SyncUsersForm"; +import {ChevronLeftIcon} from "@heroicons/react/16/solid"; +import useRoutes from "@/hooks/useRoutes"; +import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton"; +import Hr from "@/components/ui/Hr" + +export default function EditDishPage({ params }: { params: Promise<{ id: number }> }) { + const { id } = use(params) + const [dish, setDish] = useState(null) + const [isLoading, setIsLoading] = useState(true); + const routes = useRoutes(); + + const loadDish = useCallback(async () => { + try { + const fetchedDish = await fetchDish(id); + setDish(fetchedDish); + } catch (error) { + console.error("Error fetching dish:", error); + throw new Error('No token found in localStorage.'); + } finally { + setIsLoading(false); + } + }, [id]); + + useEffect(() => { + loadDish(); + }, [loadDish]); + + + if (isLoading || dish === null) { + return + } + + return ( +
+
+ Edit Dish + + +

BACK

+
+
+ + + +
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/archive/src/app/dishes/create/page.tsx b/frontend/archive/src/app/dishes/create/page.tsx new file mode 100644 index 0000000..156c038 --- /dev/null +++ b/frontend/archive/src/app/dishes/create/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import CreateDishForm from "@/components/features/dishes/CreateDishForm"; + +export default function CreateDishPage() { + return +} \ No newline at end of file diff --git a/frontend/archive/src/app/dishes/page.tsx b/frontend/archive/src/app/dishes/page.tsx new file mode 100644 index 0000000..274a848 --- /dev/null +++ b/frontend/archive/src/app/dishes/page.tsx @@ -0,0 +1,57 @@ +"use client"; + +import PageTitle from "@/components/ui/PageTitle"; +import {DishType} from "@/types/DishType"; +import Dish from "@/components/features/dishes/Dish"; +import {PlusIcon} from "@heroicons/react/24/solid"; +import {useEffect, useState} from "react"; +import useRoutes from "@/hooks/useRoutes"; +import {listDishes} from "@/utils/api/dishApi"; +import Button from "@/components/ui/Button" +export const dynamic = 'force-dynamic'; + +export default function DishesIndexPage() { + const routes = useRoutes(); + + const [dishes, setDishes] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + listDishes() + .then((dishes: DishType[]) => setDishes(dishes)) + .finally(() => setLoading(false)); + }, []); + + if (loading) return

Loading...

; + + if (! dishes) { + return

Loading...

+ } + + return ( + <> +
+
+ Dishes +
+
+ +
+
+ + { + dishes.length === 0 + ?

No dishes found :(

+ : dishes.map((dish: DishType, index: number) => ) + } + + ); +} diff --git a/frontend/archive/src/app/favicon.ico b/frontend/archive/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/archive/src/app/layout.tsx b/frontend/archive/src/app/layout.tsx new file mode 100644 index 0000000..f6d242b --- /dev/null +++ b/frontend/archive/src/app/layout.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import type { Metadata } from 'next'; +import NavBar from '@/components/layout/NavBar'; +import { AuthProvider } from '@/context/AuthContext'; +import AuthGuard from "@/components/layout/AuthGuard"; +import '@/styles/main.css'; + +export const metadata: Metadata = { + title: 'DishPlanner', + description: 'Schedule your dishes', +}; + +export default function RootLayout({children,}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + Dish Planner + + + + + +
{children}
+
+
+ + + ); +} diff --git a/frontend/archive/src/app/login/page.tsx b/frontend/archive/src/app/login/page.tsx new file mode 100644 index 0000000..cf92a57 --- /dev/null +++ b/frontend/archive/src/app/login/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import LoginForm from "@/components/features/auth/LoginForm"; + +export default function LoginPage() { + return ( +
+ +
+ ); +} diff --git a/frontend/archive/src/app/page.tsx b/frontend/archive/src/app/page.tsx new file mode 100644 index 0000000..8c98217 --- /dev/null +++ b/frontend/archive/src/app/page.tsx @@ -0,0 +1,9 @@ +import UpcomingDishes from "@/components/features/schedule/UpcomingDishes"; +export const dynamic = 'force-dynamic'; + +export default async function FrontPage() { + + return ( + + ); +} diff --git a/frontend/archive/src/app/register/page.tsx b/frontend/archive/src/app/register/page.tsx new file mode 100644 index 0000000..1cad575 --- /dev/null +++ b/frontend/archive/src/app/register/page.tsx @@ -0,0 +1,14 @@ +'use client'; + +import RegistrationForm from "@/components/features/auth/RegistrationForm"; + +const RegistrationPage = () => { + return ( +
+ +
+ ); + +} + +export default RegistrationPage \ No newline at end of file diff --git a/frontend/archive/src/app/schedule/[date]/edit/page.tsx b/frontend/archive/src/app/schedule/[date]/edit/page.tsx new file mode 100644 index 0000000..a1d3097 --- /dev/null +++ b/frontend/archive/src/app/schedule/[date]/edit/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { use } from "react"; +import ScheduleEditForm from "@/components/features/schedule/ScheduleEditForm"; + +const ScheduleEditPage = ({ params }: { params: Promise<{ date: string }> }) => { + const { date } = use(params) + + return +} + +export default ScheduleEditPage \ No newline at end of file diff --git a/frontend/archive/src/app/scheduled-user-dishes/history/page.tsx b/frontend/archive/src/app/scheduled-user-dishes/history/page.tsx new file mode 100644 index 0000000..aa6f772 --- /dev/null +++ b/frontend/archive/src/app/scheduled-user-dishes/history/page.tsx @@ -0,0 +1,9 @@ +import HistoricalDishes from "@/components/features/schedule/HistoricalDishes"; +export const dynamic = 'force-dynamic'; + +export default async function HistoryPage() { + + return ( + + ); +} diff --git a/frontend/archive/src/app/users/[id]/edit/page.tsx b/frontend/archive/src/app/users/[id]/edit/page.tsx new file mode 100644 index 0000000..3545ae0 --- /dev/null +++ b/frontend/archive/src/app/users/[id]/edit/page.tsx @@ -0,0 +1,30 @@ +'use client' + +import {FC, use, useEffect, useState} from "react"; +import {UserType} from "@/types/UserType"; +import {showUser} from "@/utils/api/usersApi"; +import Spinner from "@/components/Spinner"; +import EditUserForm from "@/components/features/users/EditUserForm"; +export const dynamic = 'force-dynamic'; + +interface Props { + params: Promise<{ id: number }>; +} + +const UpdateUsersPage: FC = ({ params }) => { + const { id } = use(params) + const [user, setUser] = useState(null) + + useEffect(() => { + showUser(id) + .then((user: UserType) => setUser(user)) + }, [id]); + + if (!user) { + return + } + + return +} + +export default UpdateUsersPage; \ No newline at end of file diff --git a/frontend/archive/src/app/users/create/page.tsx b/frontend/archive/src/app/users/create/page.tsx new file mode 100644 index 0000000..80480f9 --- /dev/null +++ b/frontend/archive/src/app/users/create/page.tsx @@ -0,0 +1,60 @@ +'use client' + +import PageTitle from "@/components/ui/PageTitle"; +import useRoutes from "@/hooks/useRoutes"; +import {useRouter} from "next/navigation"; +import {useState} from "react"; +import Alert from "@/components/ui/Alert"; +import {createUser} from "@/utils/api/usersApi"; +import Link from "next/link"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; +export const dynamic = 'force-dynamic'; + +const CreateUsersPage = () => { + const [name, setName] = useState(''); + const [error, setError] = useState(''); + const router = useRouter(); + const routes = useRoutes(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!name.trim()) { + setError('Name cannot be empty.'); + return; + } + + createUser(name) + .then(() => { + router.push(routes.user.index()) + }) + } + + return ( +
+ Create User + Back to users + +
+ { + error != '' && { error } + } + + + setName(e.target.value)} + className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + + Create +
+
+ ); +} + +export default CreateUsersPage; \ No newline at end of file diff --git a/frontend/archive/src/app/users/page.tsx b/frontend/archive/src/app/users/page.tsx new file mode 100644 index 0000000..0943dd8 --- /dev/null +++ b/frontend/archive/src/app/users/page.tsx @@ -0,0 +1,78 @@ +'use client' + +import PageTitle from "@/components/ui/PageTitle"; +import {useFetchUsers} from "@/hooks/useFetchUsers"; +import Spinner from "@/components/Spinner"; +import useRoutes from "@/hooks/useRoutes"; +import Link from "next/link"; +import {PencilIcon, PlusIcon, TrashIcon} from "@heroicons/react/24/solid"; +import React from "react"; +import {deleteUser} from "@/utils/api/usersApi"; +import {UserType} from "@/types/UserType"; +import Card from "@/components/layout/Card"; +import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton"; + +const UsersPage = () => { + const { users, isLoading } = useFetchUsers(); + const routes = useRoutes(); + + const handleDelete = (user: UserType) => { + deleteUser(user) + .then(() => window.location.reload()) + } + + if (isLoading) { + return ; + } + + const usersList = () => { + return users.map((user) => ( + +
+ {user.name} +
+
+
+ +
+ +
+ +
+
+ handleDelete(user)}> +
+ +
+ +
+
+
+ )) + }; + + return ( +
+
+
+ Users +
+ +
+ + +

Add User

+
+
+
+ + { + users && users.length > 0 + ? usersList() + :
No users
+ } +
+ ); +} + +export default UsersPage; \ No newline at end of file diff --git a/frontend/archive/src/components/Spinner.tsx b/frontend/archive/src/components/Spinner.tsx new file mode 100644 index 0000000..f170616 --- /dev/null +++ b/frontend/archive/src/components/Spinner.tsx @@ -0,0 +1,15 @@ +const Spinner = () => { + + return ( +
+ + + + +
+ ) + +} + +export default Spinner \ No newline at end of file diff --git a/frontend/archive/src/components/features/OnboardingBanner.tsx b/frontend/archive/src/components/features/OnboardingBanner.tsx new file mode 100644 index 0000000..9bbc003 --- /dev/null +++ b/frontend/archive/src/components/features/OnboardingBanner.tsx @@ -0,0 +1,49 @@ +import { FC } from "react" +import Link from "next/link" +import useRoutes from "@/hooks/useRoutes" +import { UserType } from "@/types/UserType" +import { DishType } from "@/types/DishType" + +interface Props { + dishes: DishType[], + users: UserType[] +} + +const OnboardingBanner: FC = ({ dishes, users }) => { + const routes = useRoutes(); + + const steps = [ + { + label: "Create a user", + href: routes.user.create(), + count: users.length + }, { + label: "Create a dish", + href: routes.dish.create(), + count: dishes.length + } + ] + + return ( +
+
Welcome to DishPlanner
+
To get you started, please follow these steps to set up your account. This will ensure a better + experience. +
+ + { + steps.map((step, index) => ( +
+ { + step.count === 0 + ? { step.label } + :
{ step.label }
+ } +
+ )) + } +
+ ) +} + +export default OnboardingBanner; \ No newline at end of file diff --git a/frontend/archive/src/components/features/auth/LoginForm.tsx b/frontend/archive/src/components/features/auth/LoginForm.tsx new file mode 100644 index 0000000..2189c36 --- /dev/null +++ b/frontend/archive/src/components/features/auth/LoginForm.tsx @@ -0,0 +1,96 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useAuth } from '@/context/AuthContext'; +import { login } from "@/utils/api/auth"; +import { useRouter } from 'next/navigation'; +import Link from "next/link"; +import useRoutes from "@/hooks/useRoutes"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; +import { useSearchParams } from 'next/navigation'; +import Alert from "@/components/ui/Alert"; + +export default function LoginForm() { + const { login: authLogin } = useAuth(); + const router = useRouter(); + const routes = useRoutes(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const searchParams = useSearchParams(); + const [alertSuccess, setAlertSuccess] = useState([]) + + // handle registration success message + const isRegistered = searchParams.get('registered') === 'true'; + + useEffect(() => { + if (isRegistered) { + setAlertSuccess(['Registration successful!',' You can now log in.']); + + const timer = setTimeout(() => { + const params = new URLSearchParams(searchParams.toString()); + params.delete('registered'); + const newUrl = `${window.location.pathname}?${params.toString()}`; + + router.replace(newUrl); + }, 3000); + + return () => clearTimeout(timer) + } + }, [isRegistered, router, searchParams]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + + try { + await login(email, password); + authLogin(); + router.replace('/'); + } catch (err) { + const errorMessage = + err instanceof Error + ? err.message + : 'Login failed'; + setError(errorMessage); + } + }; + + return ( +
+ { alertSuccess.length > 0 && + + {alertSuccess.map((msg, index) => ( + + {msg} +
+
+ ))} +
+ } +
+ {error &&

{error}

} + setEmail(e.target.value)} + required + className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + setPassword(e.target.value)} + required + className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + Login + + Create an account + +
+
+ ); +} diff --git a/frontend/archive/src/components/features/auth/RegistrationForm.tsx b/frontend/archive/src/components/features/auth/RegistrationForm.tsx new file mode 100644 index 0000000..0738381 --- /dev/null +++ b/frontend/archive/src/components/features/auth/RegistrationForm.tsx @@ -0,0 +1,104 @@ +'use client'; + +import React, { useState } from 'react'; +import { register } from "@/utils/api/auth"; +import { useRouter } from 'next/navigation'; +import useRoutes from "@/hooks/useRoutes"; +import Link from "next/link"; +import SectionTitle from "@/components/ui/SectionTitle"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; + +export default function LoginForm() { + const router = useRouter(); + const routes = useRoutes(); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordAgain, setPasswordAgain] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isRegistered, setIsRegistered] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (password !== passwordAgain) { + setError("Passwords do not match."); + return; + } + + try { + setIsLoading(true); + + await register(name, email, password, passwordAgain); + + router.replace('/login?registered=true'); + } catch (err) { + const errorMessage = + err instanceof Error + ? err.message + : 'Registration\n failed'; + setError(errorMessage); + } finally { + setIsRegistered(true); + setIsLoading(false); + } + }; + + if (isRegistered) { + return
+ Registration successful! + Please continue to the login page. +
+ } + + return ( +
+
+

Register

+ { error &&

{ error }

} + setName(e.target.value) } + required + className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + setEmail(e.target.value) } + required + className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + setPassword(e.target.value) } + required + className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + setPasswordAgain(e.target.value) } + required + className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + + Create Account + + + Back to Login + +
+
+ ); +} diff --git a/frontend/archive/src/components/features/dishes/AddUserToDishForm.tsx b/frontend/archive/src/components/features/dishes/AddUserToDishForm.tsx new file mode 100644 index 0000000..2e2d2c8 --- /dev/null +++ b/frontend/archive/src/components/features/dishes/AddUserToDishForm.tsx @@ -0,0 +1,101 @@ +import React, { FC, useState } from "react"; +import { DishType } from "@/types/DishType"; +import { UserType } from "@/types/UserType"; +import { useFetchUsers } from "@/hooks/useFetchUsers"; +import Spinner from "@/components/Spinner"; +import {addUserToDish} from "@/utils/api/dishApi"; +import OutlineButton from "@/components/ui/Buttons/OutlineButton"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; + +interface Props { + dish: DishType; + reloadDish: () => void; +} + +const AddUserToDishForm: FC = ({ dish, reloadDish }) => { + const [showAdd, setShowAdd] = useState(false); + const [selectedUser, setSelectedUser] = useState("-1"); + const { users, isLoading: isUsersLoading } = useFetchUsers(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (selectedUser === "-1") { + alert("Please select a valid user."); + return; + } + + const userToAdd = users.find((user: UserType) => user.id === parseInt(selectedUser)); + + if (!userToAdd) { + alert("User not found."); + return; + } + + addUserToDish(dish.id, userToAdd.id) + .then(() => { + setShowAdd(false); + setSelectedUser("-1"); + reloadDish(); + }) + .catch(() => { + alert("Failed to add user, please try again."); + }); + }; + + if (isUsersLoading) { + return ; + } + + const remainingUsers = users.filter( + (user: UserType) => + !dish.users.find((dishUser: UserType) => dishUser.id === user.id) + ); + + return ( + <> + setShowAdd(!showAdd)} + disabled={remainingUsers.length === 0} + type="button" + > + Add User + + + { showAdd && ( +
+
+
+
+ +
+ +
+ + Add User + +
+
+
+
+ )} + + ); +}; + +export default AddUserToDishForm; \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/CreateDishForm.tsx b/frontend/archive/src/components/features/dishes/CreateDishForm.tsx new file mode 100644 index 0000000..242c1f2 --- /dev/null +++ b/frontend/archive/src/components/features/dishes/CreateDishForm.tsx @@ -0,0 +1,95 @@ +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { createDish } from "@/utils/api/dishApi"; +import PageTitle from "@/components/ui/PageTitle"; +import Alert from "@/components/ui/Alert"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; +import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton"; +import { ChevronLeftIcon } from "@heroicons/react/16/solid"; +import Hr from "@/components/ui/Hr" + +const CreateDishForm = () => { + const router = useRouter() + const [name, setName] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const validateForm = () => { + if (!name.trim()) { + setError("Dish name cannot be empty."); + return false; + } + + return true; + }; + + const submitForm = async (e: React.FormEvent) => { + e.preventDefault() + + // Validate client-side input + if (!validateForm()) return; + + setError(""); + setLoading(true); + + try { + const result = await createDish(name); + if (result) { + router.push('/dishes') + } + } catch (error: unknown) { + setError(error instanceof Error ? error.message : "An unexpected error occurred."); + } finally { + setLoading(false); + } + } + + return ( +
+
+ Create Dish +
+ +
+ { error && ( + { error } + ) } + +
+ + setName(e.target.value) } // Update the name state on change + className="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-secondary focus:bg-gray-900" + placeholder="Enter dish name" + /> +
+ + + { loading ? "Saving..." : "Save Changes" } + +
+ +
+ + } + > + Back to dishes + +
+ ); + + +}; + +export default CreateDishForm; \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/Dish.tsx b/frontend/archive/src/components/features/dishes/Dish.tsx new file mode 100644 index 0000000..7933a41 --- /dev/null +++ b/frontend/archive/src/components/features/dishes/Dish.tsx @@ -0,0 +1,39 @@ +import {DishType} from "@/types/DishType"; + +import {PencilIcon, TrashIcon} from '@heroicons/react/24/solid' +import Link from "next/link"; +import useRoutes from "@/hooks/useRoutes"; +import {UserType} from "@/types/UserType"; +import Card from "@/components/layout/Card"; + +const Dish = ({ dish }: { dish: DishType}) => { + const routes = useRoutes(); + + return ( + +
+

{ dish.name }

+ + { + dish.users.map((user: UserType) => ( +
{user.name.slice(0, 1)}
+ )) + } +
+
+ +
+ +
+ + +
+ +
+ +
+
+ ) +} + +export default Dish \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/DishCard.tsx b/frontend/archive/src/components/features/dishes/DishCard.tsx new file mode 100644 index 0000000..66b2c52 --- /dev/null +++ b/frontend/archive/src/components/features/dishes/DishCard.tsx @@ -0,0 +1,23 @@ +import {UserType} from "@/types/UserType"; +import {FC} from "react"; +import {DishType} from "@/types/DishType"; + +interface Props { + user: UserType, + dish: DishType, +} + +const DishCard: FC = ({ user, dish }: Props) => { + return ( +
+
+ { user.name.slice(0, 1) } +
+
+ { dish ? dish.name : '-' } +
+
+ ) +} + +export default DishCard \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/EditDishForm.tsx b/frontend/archive/src/components/features/dishes/EditDishForm.tsx new file mode 100644 index 0000000..cfffb2a --- /dev/null +++ b/frontend/archive/src/components/features/dishes/EditDishForm.tsx @@ -0,0 +1,87 @@ +import React, {FC, useState} from "react"; +import {useRouter} from "next/navigation"; +import Alert from "@/components/ui/Alert"; +import {updateDish} from "@/utils/api/dishApi"; +import {DishType} from "@/types/DishType"; +import useRoutes from "@/hooks/useRoutes"; +import Spinner from "@/components/Spinner"; +import Button from "@/components/ui/Button" + +interface Props { + dish: DishType +} + +const EditDishForm: FC = ({ dish }) => { + const [name, setName] = useState(dish.name); + const [error, setError] = useState(""); + const router = useRouter() + const [loading, setLoading] = useState(false); + const routes = useRoutes(); + + const validateForm = () => { + if (!name.trim()) { + setError("Dish name cannot be empty."); + return false; + } + + return true; + }; + + const submitForm = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validateForm()) return; + + setError(""); + setLoading(true); + + try { + const result = await updateDish(dish.id, name); + if (result) { + router.push(routes.dish.index()) + } + } catch (error: unknown) { + setError(error instanceof Error ? error.message : "An unexpected error occurred"); + } finally { + setLoading(false); // Reset loading state + } + } + + if (loading) { + return ; + } + + return ( +
+ { + error != '' && { error } + } + + {/* Dish name input */} +
+ + setName(e.target.value)} // Update the name state on change + className="p-2 border rounded w-full bg-gray-500 border-secondary background-secondary" + /> +
+ + {/* Save button */} + +
+ ); +} + +export default EditDishForm; \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/EditDishUserCardEditForm.tsx b/frontend/archive/src/components/features/dishes/EditDishUserCardEditForm.tsx new file mode 100644 index 0000000..eb4bc06 --- /dev/null +++ b/frontend/archive/src/components/features/dishes/EditDishUserCardEditForm.tsx @@ -0,0 +1,119 @@ +import React, {FC} from "react"; +import SectionTitle from "@/components/ui/SectionTitle"; +import {syncUserDishRecurrences} from "@/utils/api/usersApi"; +import Spinner from "@/components/Spinner"; +import {UserDishType} from "@/types/ScheduledUserDishType"; +import {RecurrenceType} from "@/types/ScheduleType"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; + +interface Props { + userDish: UserDishType + onSubmit: () => void +} + +const EditDishUserCardEditForm: FC = ({ userDish, onSubmit}) => { + const weeklyRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\WeeklyRecurrence') + const minimumRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\MinimumRecurrence') + + const wv = weeklyRecurrence ? weeklyRecurrence.value : undefined + const mv = minimumRecurrence ? minimumRecurrence.value : undefined + + const [isWeeklyOn, setIsWeeklyOn] = React.useState(weeklyRecurrence !== undefined); + const [isMinimumOn, setIsMinimumOn] = React.useState(minimumRecurrence !== undefined); + const [weekday, setWeekday] = React.useState(wv ?? 0); + const [minimumValue, setMinimumValue] = React.useState(mv ?? 7); + const [loading, setLoading] = React.useState(false); + + const handleSubmit = () => { + const recurrences = [] + + if (isWeeklyOn) { + recurrences.push({ + type: 'App\\Models\\WeeklyRecurrence', + value: weekday, + }); + } + + if (isMinimumOn) { + recurrences.push({ + type: 'App\\Models\\MinimumRecurrence', + value: minimumValue, + }); + } + + setLoading(true) + syncUserDishRecurrences(userDish.dish.id, userDish.user.id, recurrences as RecurrenceType[]) + .then((data) => console.log('request data', data)) + .finally(() => { + setLoading(false) + onSubmit() + }) + } + + if (loading) { + return ; + } + + return ( +
+ Recurrences + +
+
+ setIsWeeklyOn(!isWeeklyOn)} + className="w-4 h-4 border border-gray-300 rounded-sm bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" + /> + +
+ { + isWeeklyOn && ( +
+ + +
+ ) + } +
+ +
+
+ setIsMinimumOn(!isMinimumOn)} + className="w-4 h-4 border border-gray-300 rounded-sm bg-gray-500 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" + /> + +
+ + { + isMinimumOn && ( +
+ setMinimumValue(parseInt(e.currentTarget.value))} min="0" max="365" className="background-secondary border-secondary border-2 w-12 px-2" /> + +
+ ) + } +
+ + Save +
+ ); +} + +export default EditDishUserCardEditForm; \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/RecurrenceLabels.tsx b/frontend/archive/src/components/features/dishes/RecurrenceLabels.tsx new file mode 100644 index 0000000..bba81ba --- /dev/null +++ b/frontend/archive/src/components/features/dishes/RecurrenceLabels.tsx @@ -0,0 +1,54 @@ +import {FC} from "react"; +import {RecurrenceType} from "@/types/ScheduleType"; + +interface Props { + recurrences: RecurrenceType[]; +} + +const RecurrenceLabels: FC = ({recurrences}) => { + const weeklyRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\WeeklyRecurrence'); + const minimumRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\MinimumRecurrence'); + + const renderWeeklyRecurrence = () => { + if (weeklyRecurrences == undefined || weeklyRecurrences.length == 0) { + return ''; + } + + const weekdayString = (() => { + switch (weeklyRecurrences[0].value) { + case 0: return "Sunday" + case 1: return "Monday" + case 2: return "Tuesday"; + case 3: return "Wednesday"; + case 4: return "Thursday"; + case 5: return "Friday"; + case 6: return "Saturday"; + default: return "Invalid day"; + } + }) + + return ( +
+ { weekdayString() } +
+ ) + } + const renderMinimumRecurrence = () => { + if (minimumRecurrences == undefined || minimumRecurrences.length == 0) { + return ''; + } + + return ( +
+ min: { minimumRecurrences[0].value } +
+ ) + } + + return <> + { renderWeeklyRecurrence() } + { renderMinimumRecurrence() } + ; +}; + +export default RecurrenceLabels; \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/SyncUsersForm.tsx b/frontend/archive/src/components/features/dishes/SyncUsersForm.tsx new file mode 100644 index 0000000..296167d --- /dev/null +++ b/frontend/archive/src/components/features/dishes/SyncUsersForm.tsx @@ -0,0 +1,32 @@ +import React, { FC } from "react"; +import { DishType } from "@/types/DishType"; +import { UserType } from "@/types/UserType"; +import UserDishCard from "@/components/features/dishes/UserDishCard"; +import SectionTitle from "@/components/ui/SectionTitle"; +import AddUserToDishForm from "@/components/features/dishes/AddUserToDishForm"; + +interface Props { + dish: DishType; + reloadDish: () => void; +} + +const SyncUsersForm: FC = ({ dish, reloadDish }) => { + return ( +
+ Users + + + + {dish.users.map((user: UserType) => ( + + ))} +
+ ); +}; + +export default SyncUsersForm; \ No newline at end of file diff --git a/frontend/archive/src/components/features/dishes/UserDishCard.tsx b/frontend/archive/src/components/features/dishes/UserDishCard.tsx new file mode 100644 index 0000000..4e06e8c --- /dev/null +++ b/frontend/archive/src/components/features/dishes/UserDishCard.tsx @@ -0,0 +1,83 @@ +import React, {FC, useEffect} from "react"; +import {DishType} from "@/types/DishType"; +import {UserType} from "@/types/UserType"; +import Link from "next/link"; +import {PencilIcon, TrashIcon} from "@heroicons/react/24/solid"; +import {removeUserFromDish} from "@/utils/api/dishApi"; +import EditDishUserCardEditForm from "@/components/features/dishes/EditDishUserCardEditForm"; +import {getUserDishForUserAndDish} from "@/utils/api/usersApi"; +import Spinner from "@/components/Spinner"; +import RecurrenceLabels from "@/components/features/dishes/RecurrenceLabels"; +import {UserDishType} from "@/types/ScheduledUserDishType"; + +interface Props { + dish: DishType + user: UserType + reloadDish: () => void +} + +const UserDishCard: FC = ({dish, user, reloadDish}) => { + const [userDish, setUserDish] = React.useState(null); + const [userDishLoading, setUserDishLoading] = React.useState(true); + const [isEditMode, setIsEditMode] = React.useState(false); + + useEffect(() => { + getUserDishForUserAndDish(user.id, dish.id) + .then((userDish) => setUserDish(userDish)) + .finally(() => setUserDishLoading(false)) + }, [dish, user]); + + const handleRemove = () => { + removeUserFromDish(dish.id, user.id) + .then(() => reloadDish()) + .catch(() => { + alert("Failed to remove user, please try again."); + }); + }; + + if (userDishLoading || !userDish) { + return + } + + const onUserCardSubmit = () => { + setIsEditMode(false); + reloadDish() + } + + return ( +
+
+
+ {user.name} +
+ +
+ +
+ +
+ setIsEditMode(!isEditMode)} href="#"> +
+ +
+ +
+
+ +
+ +
+ +
+
+ + {isEditMode && ( +
+ +
+ )} +
+ ); +} + +export default UserDishCard; \ No newline at end of file diff --git a/frontend/archive/src/components/features/navbar/MobileDropdownMenu.tsx b/frontend/archive/src/components/features/navbar/MobileDropdownMenu.tsx new file mode 100644 index 0000000..3e8639a --- /dev/null +++ b/frontend/archive/src/components/features/navbar/MobileDropdownMenu.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; +import React, {FC} from "react"; +import useRoutes from "@/hooks/useRoutes"; +import classNames from "classnames"; + +interface Props { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + handleLogout: (e: React.MouseEvent) => void; +} + +const divStyles = classNames( + 'absolute', 'text-xxl', 'rounded-b', 'top-full mt-1', 'left-0', 'right-0', '', 'py-2', + 'bg-gray-600', 'border-secondary', 'shadow-md', 'flex', 'flex-col', 'space-y-3', + 'md:hidden' +) + +const linkStyles = classNames( + 'border-b-2', 'border-secondary', 'uppercase', + 'text-primary', 'hover:background-secondary', 'pb-2', 'pl-5', + 'space-grotesk', 'text-xl' +) + +const MobileDropdownMenu: FC = ({ isOpen, setIsOpen, handleLogout }) => { + const routes = useRoutes(); + + if (!isOpen) return null; + + return ( +
+ setIsOpen(false)} + > + Home + + setIsOpen(false)} + > + Dishes + + setIsOpen(false)} + > + Users + + setIsOpen(false)} + > + History + + + Logout + +
+ ) +} + +export default MobileDropdownMenu \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/HistoricalDishes.tsx b/frontend/archive/src/components/features/schedule/HistoricalDishes.tsx new file mode 100644 index 0000000..ad3718f --- /dev/null +++ b/frontend/archive/src/components/features/schedule/HistoricalDishes.tsx @@ -0,0 +1,44 @@ +"use client" + +import {useEffect, useState} from "react"; +import {DateTime} from "luxon"; +import ScheduleCalendar from "@/components/features/schedule/ScheduleCalendar"; +import PageTitle from "@/components/ui/PageTitle"; +import {ScheduleType} from "@/types/ScheduleType"; +import Spinner from "@/components/Spinner"; +import {listSchedule} from "@/utils/api/scheduleApi"; + +const HistoricalDishes = () => { + const [schedule, setSchedule] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const yesterday = DateTime.now().minus({ days: 1 }).toFormat('yyyy-LL-dd'); + + useEffect(() => { + listSchedule(undefined, yesterday) + .then((dishes: ScheduleType[]) => dishes + .sort((a: ScheduleType, b: ScheduleType) => new Date(b.date).getTime() - new Date(a.date).getTime()) + ) + .then((dishes) => setSchedule(dishes)) + .finally(() => setIsLoading(false)) + }, [yesterday]); + + if (isLoading) { + return ; + } + + if (!schedule || Object.keys(schedule).length === 0) { + return ( +
+ No dishes scheduled +
+ ); + } + + return
+ History + +
+} + +export default HistoricalDishes \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/ScheduleCalendar.tsx b/frontend/archive/src/components/features/schedule/ScheduleCalendar.tsx new file mode 100644 index 0000000..b146f6a --- /dev/null +++ b/frontend/archive/src/components/features/schedule/ScheduleCalendar.tsx @@ -0,0 +1,75 @@ +"use client" + +import {FC} from "react"; +import ScheduleDayCard from "@/components/features/schedule/dayCard/ScheduleDayCard"; +import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; +import {useFetchUsers} from "@/hooks/useFetchUsers"; +import Spinner from "@/components/Spinner"; + +const generateDates = (startDate: string, days: number): string[] => { + const dates = []; + const start = new Date(startDate); + + for (let i = 0; i < days; i++) { + const currentDate = new Date(start); + currentDate.setDate(start.getDate() + i); + dates.push(currentDate.toISOString().split('T')[0]); + } + + return dates; +}; + + +const fillCalendar = (schedules: ScheduleType[]): FilledScheduleType[] => { + /* +Array(14) + 0: + date: "2025-05-05" + id: 2 + is_skipped: false + scheduled_user_dishes: [] + */ + + const dates = generateDates((new Date()).toISOString().split('T')[0], 31) + + return dates.map((date): FilledScheduleType => { + console.log(date) + + const schedule = schedules.find((schedule: ScheduleType) => schedule.date == date) + + if (schedule) { + return schedule + } + + return { + date, + scheduled_user_dishes: [] + } + }) +} + +interface Props { + schedule: ScheduleType[]; +} + +const ScheduleCalendar: FC = ({ schedule }: Props) => { + const {users, isLoading: areUsersLoading} = useFetchUsers(); + + if (areUsersLoading) return + + const fullCalendar = fillCalendar(schedule) + + return ( +
+ { fullCalendar.map((schedule, index) => ( + + ))} +
+ ) +} + +export default ScheduleCalendar \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/ScheduleEditForm.tsx b/frontend/archive/src/components/features/schedule/ScheduleEditForm.tsx new file mode 100644 index 0000000..0f436a9 --- /dev/null +++ b/frontend/archive/src/components/features/schedule/ScheduleEditForm.tsx @@ -0,0 +1,142 @@ +"use client"; + +import React, { FC, useEffect, useState } from "react"; +import { ScheduleType } from "@/types/ScheduleType"; +import Spinner from "@/components/Spinner"; +import PageTitle from "@/components/ui/PageTitle"; +import { getScheduleForDate, scheduleUserDish, updateScheduleForDate } from "@/utils/api/scheduleApi"; +import { UserDishType } from "@/types/ScheduledUserDishType"; +import Label from "@/components/ui/Label"; +import SectionTitle from "@/components/ui/SectionTitle"; +import { useFetchUsers } from "@/hooks/useFetchUsers"; +import { listUserDishes } from "@/utils/api/userDishApi"; +import scheduleBuilder from "@/utils/scheduleBuilder"; +import transformDate from "@/utils/dateBuilder"; +import { ChevronLeftIcon } from "@heroicons/react/16/solid"; +import Hr from "@/components/ui/Hr" +import Button from "@/components/ui/Button" + +interface Props { + date: string; +} + +const ScheduleEditForm: FC = ({ date }) => { + const [schedule, setSchedule] = useState() + const [userDishes, setUserDishes] = useState([]) + const [isScheduleLoading, setIsScheduleLoading] = useState(true); + const [areUserDishesLoading, setAreUserDishesLoading] = useState(true); + const { users } = useFetchUsers(); + + useEffect(() => { + getScheduleForDate(date) + .then((sched: ScheduleType) => setSchedule(sched)) + .finally(() => setIsScheduleLoading(false)) + }, [date]); + + + useEffect(() => { + listUserDishes() + .then((user_dishes: UserDishType[]) => setUserDishes(user_dishes)) + .finally(() => setAreUserDishesLoading(false)) + }, []); + + const handleSkipDay = () => { + updateScheduleForDate(date, true) + .then((schedule: ScheduleType) => { + setSchedule(schedule) + }) + } + + const handleUnskipDay = () => { + updateScheduleForDate(date, false) + .then((schedule: ScheduleType) => { + setSchedule(schedule) + }) + } + + const handleChange = (e: React.ChangeEvent, userId: number) => { + const userDishId = parseInt(e.currentTarget.value); + + if (userDishId === 0) { + scheduleUserDish(date, userId, null, true).then(() => window.location.reload()); + return; + } + + scheduleUserDish(date, userId, userDishId).then(() => window.location.reload()); + } + + if (isScheduleLoading || areUserDishesLoading || !schedule) { + return + } + + const scheduleData = scheduleBuilder(schedule, users, userDishes) + + return
+
+
+ Edit Day +
+
+ { transformDate(schedule.date) } +
+
+ +
+ + { + userDishes.length === 0 &&
+
No dishes found assigned to this user.
+
Go ahead and add some first, or choose to skip the day.
+
(dishes ={`>`} edit ={`>`} add user)
+
+ } + + { schedule.is_skipped + ? + : ( + <> + { + scheduleData + .map((scheduleData) =>
+
{ scheduleData.user.name }
+
+ +
+
) + } + + ) + } + +
Changes are saved automatically
+ +
+ +
+ +
{ + schedule.is_skipped + ? + : + }
+
+
+} + +export default ScheduleEditForm \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/ScheduleRegenerateButton.tsx b/frontend/archive/src/components/features/schedule/ScheduleRegenerateButton.tsx new file mode 100644 index 0000000..5b60c2b --- /dev/null +++ b/frontend/archive/src/components/features/schedule/ScheduleRegenerateButton.tsx @@ -0,0 +1,35 @@ +import {FC, useState} from "react"; +import Modal from "@/components/ui/Modal"; +import ScheduleRegenerateForm from "@/components/features/schedule/ScheduleRegenerateForm"; +import {ArrowPathIcon} from "@heroicons/react/16/solid"; + +interface ScheduleRegenerateButtonProps { + onModalClose?: () => void; +} + +const ScheduleRegenerateButton: FC = ({ onModalClose }) => { + const [open, setOpen] = useState(false); + + const handleCloseModal = () => { + setOpen(false) + if (onModalClose) { + onModalClose() + } + } + + const modalChildren = handleCloseModal()}/> + const buttonChild =
+
+ + return +}; + +export default ScheduleRegenerateButton; \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/ScheduleRegenerateForm.tsx b/frontend/archive/src/components/features/schedule/ScheduleRegenerateForm.tsx new file mode 100644 index 0000000..017b8c2 --- /dev/null +++ b/frontend/archive/src/components/features/schedule/ScheduleRegenerateForm.tsx @@ -0,0 +1,78 @@ +import {DialogTitle} from "@headlessui/react"; +import Toggle from "@/components/ui/Toggle"; +import {FC, useEffect, useState} from "react"; +import {generateSchedule} from "@/utils/api/scheduleApi"; +import Alert from "@/components/ui/Alert"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; + +interface ScheduleRegenerateFormProps { + closeModal: () => void; +} + +const ScheduleRegenerateForm: FC = ({closeModal}) => { + const [overwrite, setOverwrite] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + }, [overwrite]); + + const close = () => { + closeModal(); + } + + const handleToggle = () => { + setOverwrite(!overwrite) + } + + const handleSubmit = () => { + generateSchedule(overwrite) + .then(() => close()) + .catch((err) => setError(err)) + } + + return <> +
+
+
+ + Regenerate Schedule + +
+
+ { + error && { error } + } +
+ +
+
+ +
+
+
+ + +
+
+
+
+ handleSubmit()} + className="inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-xs sm:ml-3 sm:w-auto" + > + Regenerate + + close()} + className="mt-3 inline-flex w-full justify-center rounded-md bg-gray-500 px-3 py-2 text-sm font-semibold text-gray-900 ring-1 shadow-xs border-secondary ring-inset sm:mt-0 sm:w-auto" + > + Cancel + +
+ ; +}; + +export default ScheduleRegenerateForm; \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/UpcomingDishes.tsx b/frontend/archive/src/components/features/schedule/UpcomingDishes.tsx new file mode 100644 index 0000000..c7731d0 --- /dev/null +++ b/frontend/archive/src/components/features/schedule/UpcomingDishes.tsx @@ -0,0 +1,62 @@ +"use client" + +import { useCallback, useEffect, useState } from "react"; +import { DateTime } from "luxon"; +import ScheduleCalendar from "@/components/features/schedule/ScheduleCalendar"; +import PageTitle from "@/components/ui/PageTitle"; +import Spinner from "@/components/Spinner"; +import { ScheduleType } from "@/types/ScheduleType"; +import { listSchedule } from "@/utils/api/scheduleApi"; +import OnboardingBanner from "@/components/features/OnboardingBanner" +import { useFetchUsers } from "@/hooks/useFetchUsers" +import { useFetchDishes } from "@/hooks/useFetchDishes" +import ScheduleRegenerateButton from "@/components/features/schedule/ScheduleRegenerateButton"; + +const UpcomingDishes = () => { + const [schedule, setSchedule] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const today = DateTime.now().toFormat("yyyy-LL-dd"); + + const fetchSchedule = useCallback(() => { + setIsLoading(true); + listSchedule(today) + .then((dishes) => setSchedule(dishes)) + .finally(() => setIsLoading(false)); + }, [today]); + + useEffect(() => { + fetchSchedule(); + }, [fetchSchedule]); + + const { users, isLoading: areUsersLoading } = useFetchUsers(); + const { dishes, isLoading: areDishesLoading } = useFetchDishes(); + + if (isLoading || areUsersLoading || areDishesLoading) { + return ; + } + + if (users.length === 0 || dishes.length === 0) { + return + } + + return ( +
+
+
+ Schedule +
+
+ +
+
+ { + !schedule || Object.keys(schedule).length === 0 + ?
No dishes scheduled
+ : + } +
+ ); +}; + +export default UpcomingDishes; \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/UserDishEditCard.tsx b/frontend/archive/src/components/features/schedule/UserDishEditCard.tsx new file mode 100644 index 0000000..b82fc1b --- /dev/null +++ b/frontend/archive/src/components/features/schedule/UserDishEditCard.tsx @@ -0,0 +1,79 @@ +import {FC, FormEvent, useMemo, useState} from "react"; +import {DishType} from "@/types/DishType"; +import {ScheduledUserDishType} from "@/types/ScheduledUserDishType"; +import {updateScheduledUserDish} from "@/utils/api/scheduledUserDishesApi"; +import Alert from "@/components/ui/Alert"; +import classNames from "classnames"; + +interface Props { + scheduledUserDish: ScheduledUserDishType + allDishes: DishType[] +} + +const UserDishEditCard: FC = ({ scheduledUserDish, allDishes }) => { + const [selectedUserDishId, setSelectedUserDishId] = useState(scheduledUserDish.user_dish ? scheduledUserDish.user_dish.id : 0) + const [errorMessage, setErrorMessage] = useState("") + const [isSuccess, setIsSuccess] = useState(false); + + const selectStyle = classNames( + 'p-2', 'rounded', 'w-full', 'background-secondary', + 'focus:outline-none', + 'transition-[border-color] ease-out duration-1000', 'border-2', // Keep consistent base styles + { + 'border-green-500': isSuccess, // Green border when successful + 'border-red-500': !isSuccess && errorMessage !== "", // Red border when there's an error + 'border-secondary': !isSuccess && errorMessage === "", // Default border for neutral state + } + ) + + const handleOnChange = (e: FormEvent) => { + const userDishId = parseInt(e.currentTarget.value); + setSelectedUserDishId(userDishId); + + updateScheduledUserDish(scheduledUserDish.id, userDishId) + .then(() => { + setIsSuccess(false); + setTimeout(() => { + setIsSuccess(true); + setTimeout(() => setIsSuccess(false), 1000); + }, 0); + }) + .catch((error) => { + setErrorMessage(error); // Log API errors + }); + }; + + const filteredDishes = useMemo(() => + allDishes.filter((dish: DishType) => + dish.users.some((user) => user.id === scheduledUserDish.user_dish.user.id) + ), + [allDishes, scheduledUserDish.user_dish.user.id] + ) + + return ( +
+
{scheduledUserDish.user_dish.user.name}
+ + { errorMessage !== "" && { errorMessage } } + + + +
+ ); +}; + +export default UserDishEditCard; \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/dayCard/DateBadge.tsx b/frontend/archive/src/components/features/schedule/dayCard/DateBadge.tsx new file mode 100644 index 0000000..9a61d87 --- /dev/null +++ b/frontend/archive/src/components/features/schedule/dayCard/DateBadge.tsx @@ -0,0 +1,27 @@ +import {DateTime} from "luxon"; +import React, {FC} from "react"; +import classNames from "classnames"; + +interface Props { + date: string + className?: string; +} + +const DateBadge: FC = ({ className, date }) => { + const isToday = DateTime.fromISO(date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd") + + const textStyle = classNames("inline font-bold", { + 'text-accent-blue': isToday, + 'text-secondary': !isToday, + }, className) + + return ( +
+
{DateTime.fromISO(date).toFormat("dd")}
+
+
{DateTime.fromISO(date).toFormat("LLL")}
+
+ ) +} + +export default DateBadge \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCard.tsx b/frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCard.tsx new file mode 100644 index 0000000..77c68e0 --- /dev/null +++ b/frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCard.tsx @@ -0,0 +1,50 @@ +import React, {FC} from "react"; +import {UserType} from "@/types/UserType"; +import ScheduleDayCardUserDish from "@/components/features/schedule/dayCard/ScheduleDayCardUserDish"; +import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; +import Link from "next/link"; +import {PencilSquareIcon} from "@heroicons/react/24/outline"; +import useRoutes from "@/hooks/useRoutes"; +import DateBadge from "@/components/features/schedule/dayCard/DateBadge"; +import { DateTime } from "luxon" +import classNames from "classnames" + +interface Props { + schedule: ScheduleType|FilledScheduleType; + users: UserType[]; +} + +const ScheduleDayCard: FC = ({schedule, users}) => { + const routes = useRoutes() + const isToday = DateTime.fromISO(schedule.date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd") + + const containerStyles = classNames( + 'w-full bg-gray-500 pt-5 pb-2 rounded-2xl text-xl', { + 'border-2 text-accent-blue border-accent-blue': isToday, + } + ) + + return ( +
+ + +
+ { + users.map((user) => ) + } + +
+ + Edit + +
+
+
+ ); +}; + +export default ScheduleDayCard; \ No newline at end of file diff --git a/frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCardUserDish.tsx b/frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCardUserDish.tsx new file mode 100644 index 0000000..64f0f0e --- /dev/null +++ b/frontend/archive/src/components/features/schedule/dayCard/ScheduleDayCardUserDish.tsx @@ -0,0 +1,32 @@ +import React, { FC } from "react"; +import { ScheduledUserDishType } from "@/types/ScheduledUserDishType"; +import { UserType } from "@/types/UserType"; +import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; + +interface Props { + schedule: ScheduleType|FilledScheduleType; + user: UserType; +} + +const ScheduleDayCardUserDish: FC = ({ schedule, user }) => { + const getDish = (user: UserType) => { + const scheduled_dishes = schedule.scheduled_user_dishes.filter((scheduled_user_dish: ScheduledUserDishType) => ( + scheduled_user_dish.user_dish?.user.id == user.id + )) + + if (scheduled_dishes.length > 0) { + return scheduled_dishes[0].user_dish.dish.name + } + + return '/' + } + + return ( +
+
{ user.name } :
+
{ getDish(user) }
+
+ ); +}; + +export default ScheduleDayCardUserDish; \ No newline at end of file diff --git a/frontend/archive/src/components/features/users/EditUserForm.tsx b/frontend/archive/src/components/features/users/EditUserForm.tsx new file mode 100644 index 0000000..1593dd9 --- /dev/null +++ b/frontend/archive/src/components/features/users/EditUserForm.tsx @@ -0,0 +1,63 @@ +import React, {FC, useState} from "react"; +import {useRouter} from "next/navigation"; +import useRoutes from "@/hooks/useRoutes"; +import {updateUser} from "@/utils/api/usersApi"; +import PageTitle from "@/components/ui/PageTitle"; +import Link from "next/link"; +import Alert from "@/components/ui/Alert"; +import {UserType} from "@/types/UserType"; +import SolidButton from "@/components/ui/Buttons/SolidButton"; + +interface Props { + user: UserType; +} + +const EditUserForm: FC = ({ user }) => { + + const [name, setName] = useState(user.name); + const [error, setError] = useState(''); + const router = useRouter(); + const routes = useRoutes(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // validateName + if (!name.trim()) { + setError('Name cannot be empty.'); + return; + } + + updateUser(user, name) + .then(() => { + router.push(routes.user.index()) + }) + } + + return ( +
+ Create User + Back to users + +
+ { + error != '' && { error } + } + + + setName(e.target.value)} + className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + /> + + Update +
+
+ ); +} + +export default EditUserForm; \ No newline at end of file diff --git a/frontend/archive/src/components/layout/AuthGuard.tsx b/frontend/archive/src/components/layout/AuthGuard.tsx new file mode 100644 index 0000000..c4e0482 --- /dev/null +++ b/frontend/archive/src/components/layout/AuthGuard.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useAuth } from '@/context/AuthContext'; +import { useRouter, usePathname } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +// Optional Loading spinner component to display while loading +const LoadingSpinner = () => ( +
+
+
+); + +export default function AuthGuard({ children }: { children: React.ReactNode }) { + const { isAuthenticated } = useAuth(); // Access the authentication state from AuthContext + const router = useRouter(); + const pathname = usePathname(); + const [loading, setLoading] = useState(true); + + // Define public routes that can be accessed without authentication + const publicRoutes = ['/login', '/register']; + const isPublic = publicRoutes.includes(pathname); + + useEffect(() => { + // Determine behavior based on auth state and route type + if (isAuthenticated === null) { + // Await authentication resolution (e.g., token check) + setLoading(true); + } else if (isAuthenticated && isPublic) { + // Redirect authenticated users away from public pages + router.replace('/'); + } else if (!isAuthenticated && !isPublic) { + // Redirect unauthenticated users trying to access protected pages + router.replace('/login'); + } else { + // Otherwise, stop loading since the state is resolved + setLoading(false); + } + }, [isAuthenticated, pathname, isPublic, router]); + + // Show a spinner while authentication state is loading + if (loading) { + return ; + } + + // Render children only when the authentication state and path are valid + return <>{children}; +} \ No newline at end of file diff --git a/frontend/archive/src/components/layout/Card.tsx b/frontend/archive/src/components/layout/Card.tsx new file mode 100644 index 0000000..6784526 --- /dev/null +++ b/frontend/archive/src/components/layout/Card.tsx @@ -0,0 +1,15 @@ +import React, {FC} from "react"; + +interface Props { + children: React.ReactNode; +} + +const Card: FC = ({ children }) => { + return ( +
+ { children } +
+ ) +} + +export default Card \ No newline at end of file diff --git a/frontend/archive/src/components/layout/NavBar.tsx b/frontend/archive/src/components/layout/NavBar.tsx new file mode 100644 index 0000000..67cadb2 --- /dev/null +++ b/frontend/archive/src/components/layout/NavBar.tsx @@ -0,0 +1,83 @@ +"use client"; + +import React, { useState } from "react"; +import Link from "next/link"; +import useRoutes from "@/hooks/useRoutes"; +import MobileDropdownMenu from "@/components/features/navbar/MobileDropdownMenu"; +import {useRouter} from "next/navigation"; +import {useAuth} from "@/context/AuthContext"; + +const NavBar = () => { + const [isOpen, setIsOpen] = useState(false); + const routes = useRoutes(); + const router = useRouter(); + const {isAuthenticated, logout} = useAuth(); + + const handleLogout = (e: React.MouseEvent) => { + e.preventDefault(); + logout(); + router.replace('/login'); + }; + + return ( + + ); +}; + +export default NavBar; \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Alert.tsx b/frontend/archive/src/components/ui/Alert.tsx new file mode 100644 index 0000000..c32070c --- /dev/null +++ b/frontend/archive/src/components/ui/Alert.tsx @@ -0,0 +1,34 @@ +import React, {FC} from "react"; +import classNames from "classnames"; + +interface Props { + children: React.ReactNode; + className?: string; + type: 'error' | 'warning' | 'info' | 'success'; +} + +const Alert: FC = ({ children, className, type } ) => { + let bgColor = 'bg-blue-200' + let fgColor = 'bg-blue-800' + + if (type == 'error') { + bgColor = 'bg-red-200' + fgColor = 'bg-red-800' + } else if (type == 'warning') { + bgColor = 'bg-orange-200' + fgColor = 'bg-orange-800' + } else if (type == 'success') { + bgColor = 'border-2 border-green-500' + fgColor = 'text-green-500' + } + + const styles = classNames(fgColor, bgColor, className, 'rounded') + + return ( +
+ { children} +
+ ) +} + +export default Alert \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Button.tsx b/frontend/archive/src/components/ui/Button.tsx new file mode 100644 index 0000000..4ad006c --- /dev/null +++ b/frontend/archive/src/components/ui/Button.tsx @@ -0,0 +1,62 @@ +import Link from "next/link"; +import React, { FC, ReactElement, ReactNode } from "react"; +import classNames from "classnames"; + +interface ButtonProps { + appearance?: 'solid' | 'outline' | 'text'; + children: ReactNode; + className?: string; + href?: string; + icon?: ReactNode; + onClick?: () => void; + disabled?: boolean; + size?: 'small' | 'medium' | 'large'; + type?: 'button' | 'submit' | 'reset'; + variant?: 'primary' | 'secondary' | 'accent'; +} + +const Button: FC = ({ appearance, children, className, disabled, href, icon, onClick, + size = 'medium', type, + variant = 'primary' +}) => { + const styles = classNames( + "flex items-center space-x-1", + "justify-center font-size-18 py-2 px-4 rounded flex", + { + 'border-2 border-primary background-red text-white': variant === 'primary' && appearance === 'solid', + 'border-2 border-primary text-primary': variant === 'primary' && appearance === 'outline', + 'text-primary': variant === 'primary' && appearance === 'text', + 'border-2 border-secondary text-secondary': variant === 'secondary' && appearance === 'outline', + 'border-2 border-accent-blue text-accent-blue': variant === 'accent' && appearance === 'outline', + }, + className + ) + + const iconClassNames = classNames({ + "h-4 w-4 mr-1": size === "small", + "h-5 w-5 mr-1": size === "medium", + "h-7 w-7 mr-2": size === "large", + }); + + const iconElement = + React.isValidElement(icon) && + React.cloneElement(icon as ReactElement<{ className?: string }>, { + className: iconClassNames, + }); + + if (href !== undefined) { + return ( + + { icon && iconElement} + { children} + + ) + } + + return +} + +export default Button \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Buttons/OutlineButton.tsx b/frontend/archive/src/components/ui/Buttons/OutlineButton.tsx new file mode 100644 index 0000000..b668b6b --- /dev/null +++ b/frontend/archive/src/components/ui/Buttons/OutlineButton.tsx @@ -0,0 +1,37 @@ +import React, { FC } from "react"; +import classNames from "classnames"; + +interface Props { + children: React.ReactNode; + className?: string; + disabled?: boolean; + onClick?: () => void; + size?: "small" | "medium" | "large"; + type: 'submit' | 'button'; +} + +const OutlineButton: FC = ({ children, className, disabled = false, onClick, size, type }) => { + const style = classNames( + "justify-center border-2 border-accent font-size-18 text-accent-blue py-2 px-4 rounded flex", + { 'text-xs': size === "small" }, + className + ) + + if (onClick === undefined) { + onClick = () => { + } + } + + return ( + + ) +} + +export default OutlineButton \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Buttons/OutlineLinkButton.tsx b/frontend/archive/src/components/ui/Buttons/OutlineLinkButton.tsx new file mode 100644 index 0000000..ab0e9b1 --- /dev/null +++ b/frontend/archive/src/components/ui/Buttons/OutlineLinkButton.tsx @@ -0,0 +1,49 @@ +import React, { FC, ReactElement } from "react"; +import classNames from "classnames"; +import Link from "next/link"; + +interface Props { + children: React.ReactNode; + className?: string; + href: string; + icon?: React.ReactNode; + size?: "small" | "medium" | "large"; + variant?: "primary" | "secondary"; +} + +const OutlineLinkButton: FC = ({ children, className, href, icon, size = "medium", variant }) => { + const linkClassNames = classNames( + "underline font-default pt-3 pb-3 px-4 rounded mb-0 flex", + { + 'text-primary border-primary': variant === "primary", + 'text-secondary border-secondary': variant === "secondary", + 'text-accent-blue border-accent': !variant || !["primary", "secondary"].includes(variant), + }, { + 'text-size-14': size === "small", + 'font-size-18': !size || size === "medium", + 'text-2xl': size === "large", + }, + className, + ) + + const iconClassNames = classNames("mt-0.5", { + "h-4 w-4 mr-1": size === "small", + "h-5 w-5 mr-1": size === "medium", // Default size + "h-7 w-7 mr-2": size === "large", + }); + + const iconElement = + React.isValidElement(icon) && + React.cloneElement(icon as ReactElement<{ className?: string }>, { + className: iconClassNames, + }); + + return ( + + {iconElement} + {children} + + ) +} + +export default OutlineLinkButton \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Buttons/SolidButton.tsx b/frontend/archive/src/components/ui/Buttons/SolidButton.tsx new file mode 100644 index 0000000..62ac01d --- /dev/null +++ b/frontend/archive/src/components/ui/Buttons/SolidButton.tsx @@ -0,0 +1,39 @@ +import React, {FC} from "react"; +import classNames from "classnames"; + +interface Props { + children: React.ReactNode; + className?: string; + disabled?: boolean; + onClick?: () => void; + size?: "small" | "medium" | "large"; + type: 'submit' | 'button'; +} + +const SolidButton: FC = ({ children, className, disabled = false, onClick, size, type }) => { + const style = classNames( + "py-2 px-4 bg-primary text-white text-xl p-2 rounded hover:bg-secondary mb-0", + { + 'text-xs' : size === "small", + 'font-size-18' : !size || size === "medium", + }, + className + ) + + if (onClick === undefined) { + onClick = () => {} + } + + return ( + + ) +} + +export default SolidButton \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Buttons/SolidLinkButton.tsx b/frontend/archive/src/components/ui/Buttons/SolidLinkButton.tsx new file mode 100644 index 0000000..77a78dd --- /dev/null +++ b/frontend/archive/src/components/ui/Buttons/SolidLinkButton.tsx @@ -0,0 +1,46 @@ +import React, { FC, ReactElement } from "react"; +import classNames from "classnames"; +import Link from "next/link"; + +interface Props { + children: React.ReactNode; + className?: string; + href: string; + icon?: React.ReactNode; + size?: "small" | "medium" | "large"; + variant?: "primary" | "secondary"; +} + +const SolidLinkButton: FC = ({ children, className, href, icon, size = "medium", variant }) => { + const style = classNames( + "py-2 px-4 text-xl p-2 rounded hover:bg-secondary mb-0 text-center flex", + { + 'background-red text-white': variant === "primary", + 'background-secondary border-2 border-secondary': variant === "secondary", + }, + className + ) + + const iconClassNames = classNames("mt-1", { + "h-4 w-4 mr-1": size === "small", + "h-5 w-5 mr-1": size === "medium", // Default size + "h-7 w-7 mr-2": size === "large", + }); + + const iconElement = + React.isValidElement(icon) && + React.cloneElement(icon as ReactElement<{ className?: string }>, { + className: iconClassNames, + }); + + return ( + +
+ {iconElement} + {children} +
+ + ) +} + +export default SolidLinkButton \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Description.tsx b/frontend/archive/src/components/ui/Description.tsx new file mode 100644 index 0000000..8b429c0 --- /dev/null +++ b/frontend/archive/src/components/ui/Description.tsx @@ -0,0 +1,17 @@ +import classNames from "classnames"; +import React from "react"; + +interface Props { + children: React.ReactNode; + className?: string; +} + +const Description = ({ children, className }: Props) => { + const style = classNames("italic font-size-16", + className + ) + + return

{ children }

+} + +export default Description \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Hr.tsx b/frontend/archive/src/components/ui/Hr.tsx new file mode 100644 index 0000000..59f0d2d --- /dev/null +++ b/frontend/archive/src/components/ui/Hr.tsx @@ -0,0 +1,14 @@ +import { FC } from "react" +import classNames from "classnames" + +interface HrProps { + className?: string; +} + +const Hr: FC = ({ className }) => { + const styles = classNames("my-4 border-secondary", className) + + return
+} + +export default Hr \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Label.tsx b/frontend/archive/src/components/ui/Label.tsx new file mode 100644 index 0000000..80bea0b --- /dev/null +++ b/frontend/archive/src/components/ui/Label.tsx @@ -0,0 +1,25 @@ +import React, {FC, ReactNode} from "react"; + +interface LabelProps { + href?: string; + children: ReactNode; + onClick?: () => void; +} + +const Label: FC = ({ href, children, onClick }) => { + const styles = "items-center space-x-1 background-accent p-2 rounded" + + if (href !== undefined) { + return ( +
+ { children} +
+ ) + } + + return +} + +export default Label \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Modal.tsx b/frontend/archive/src/components/ui/Modal.tsx new file mode 100644 index 0000000..e6e5c9c --- /dev/null +++ b/frontend/archive/src/components/ui/Modal.tsx @@ -0,0 +1,62 @@ +import {FC, JSX} from "react"; +import {Dialog, DialogBackdrop, DialogPanel} from "@headlessui/react"; +import classNames from "classnames"; +import {XMarkIcon} from "@heroicons/react/24/outline"; +import Button from "@/components/ui/Button" + +interface ModalProps { + buttonChildren?: JSX.Element; + buttonClassName?: string; + buttonLabel?: string; + modalChildren: JSX.Element; + modalOpen?: boolean; + setModalOpen: (open: boolean) => void; +} + +const Modal: FC = ({ + buttonLabel, + buttonClassName, + modalChildren, + modalOpen, + buttonChildren, + setModalOpen, +}) => { + const buttonStyles = classNames(buttonClassName, 'anta-regular'); + + const closeModal = () => { + setModalOpen(false) + } + + return ( + <> + + + + +
+
+ + closeModal()}/> + {modalChildren} + +
+
+
+ + ) +} + +export default Modal; \ No newline at end of file diff --git a/frontend/archive/src/components/ui/PageTitle.tsx b/frontend/archive/src/components/ui/PageTitle.tsx new file mode 100644 index 0000000..ca65f18 --- /dev/null +++ b/frontend/archive/src/components/ui/PageTitle.tsx @@ -0,0 +1,18 @@ +import classNames from "classnames"; +import {FC} from "react"; + +interface Props { + children: string, + className?: string, +} + +const PageTitle: FC = ({ children, className }) => { + const styles = classNames( + 'ml-4 text-2xl font-default uppercase w-full text-accent-blue font-bold', + className, + ) + + return

{ children }

+} + +export default PageTitle \ No newline at end of file diff --git a/frontend/archive/src/components/ui/RecurrenceInput.tsx b/frontend/archive/src/components/ui/RecurrenceInput.tsx new file mode 100644 index 0000000..756715d --- /dev/null +++ b/frontend/archive/src/components/ui/RecurrenceInput.tsx @@ -0,0 +1,65 @@ +import React, {FC, useState} from "react"; + +interface Props { + value: number; + setValue: (value: number) => void; +} + +const RecurrenceInput: FC = ({ value, setValue}) => { + const [openInput, setOpenInput] = useState<'category' | 'number'>([7, 365].includes(value) ? 'category' : 'number') + + const toggleInput = (e: React.MouseEvent) => { + e.preventDefault() + setOpenInput(openInput == 'category' ? 'number' : 'category') + } + + const toggleButton = () => { + return ( + + ) + } + + const prepareValue = (v: string) => { + setValue(parseInt(v)) + } + + return ( +
+
+ + + { toggleButton() } +
+ +
+ + prepareValue(e.target.value)} + className="p-2 border rounded w-full bg-gray-500 border-secondary" + /> + { toggleButton() } +
+
+ ) +} + +export default RecurrenceInput \ No newline at end of file diff --git a/frontend/archive/src/components/ui/SectionTitle.tsx b/frontend/archive/src/components/ui/SectionTitle.tsx new file mode 100644 index 0000000..93d13c4 --- /dev/null +++ b/frontend/archive/src/components/ui/SectionTitle.tsx @@ -0,0 +1,16 @@ +import classNames from "classnames"; + +interface Props { + children: string; + className?: string; +} + +const SectionTitle = ({ children, className }: Props) => { + const style = classNames("block font-size-18 uppercase w-full pl-2 text-accent-blue", + className + ) + + return

{ children }

+} + +export default SectionTitle \ No newline at end of file diff --git a/frontend/archive/src/components/ui/Toggle.tsx b/frontend/archive/src/components/ui/Toggle.tsx new file mode 100644 index 0000000..786b093 --- /dev/null +++ b/frontend/archive/src/components/ui/Toggle.tsx @@ -0,0 +1,42 @@ +import {FC} from "react"; + +interface ToggleProps { + checked: boolean; + onChange: (checked: boolean) => void; +} + +const Toggle: FC = ({ checked, onChange }) => { + const handleChange = () => { + onChange(checked); + } + + return ( + + ); +}; + +export default Toggle; \ No newline at end of file diff --git a/frontend/archive/src/context/AuthContext.tsx b/frontend/archive/src/context/AuthContext.tsx new file mode 100644 index 0000000..8d77eb3 --- /dev/null +++ b/frontend/archive/src/context/AuthContext.tsx @@ -0,0 +1,46 @@ +"use client" + +import React, { createContext, useContext, useEffect, useState } from 'react'; + +interface AuthContextProps { + isAuthenticated: boolean | null; + login: () => void; + logout: () => void; +} + +const AuthContext = createContext({ + isAuthenticated: null, + login: () => {}, + logout: () => {}, +}); + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const [isAuthenticated, setIsAuthenticated] = useState(null); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + // You could add any token validation logic here + setIsAuthenticated(true); + } else { + setIsAuthenticated(false); + } + }, []); + + const login = () => { + setIsAuthenticated(true); + }; + + const logout = () => { + setIsAuthenticated(false); + localStorage.removeItem('token'); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); \ No newline at end of file diff --git a/frontend/archive/src/helpers/Date.ts b/frontend/archive/src/helpers/Date.ts new file mode 100644 index 0000000..f183c86 --- /dev/null +++ b/frontend/archive/src/helpers/Date.ts @@ -0,0 +1,18 @@ +import { DateTime } from 'luxon'; + +// Validate if a given string matches the "yyyy-MM-dd" format and is a valid date +export const isValidDate = (date: string): boolean => { + const parsedDate = DateTime.fromFormat(date, 'yyyy-MM-dd'); + return parsedDate.isValid && parsedDate.toFormat('yyyy-MM-dd') === date; +}; + +// Format a date to a specific string format +export const formatDate = (date: Date | string, format: string = 'yyyy-MM-dd'): string => { + const parsedDate = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date); + return parsedDate.toFormat(format); +}; + +// Compare two dates to see if one is before the other +export const isBefore = (date1: string, date2: string): boolean => { + return DateTime.fromISO(date1) < DateTime.fromISO(date2); +}; diff --git a/frontend/archive/src/hooks/useFetchDishes.ts b/frontend/archive/src/hooks/useFetchDishes.ts new file mode 100644 index 0000000..a7ab747 --- /dev/null +++ b/frontend/archive/src/hooks/useFetchDishes.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from "react"; +import { listDishes } from "@/utils/api/dishApi" +import { DishType } from "@/types/DishType" + +export const useFetchDishes = () => { + const [dishes, setDishes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchDishes = async () => { + listDishes() + .then((dishes: DishType[]) => setDishes(dishes)) + .catch((err) => setError((err as Error).message || "An error occurred.")) + .finally(() => setIsLoading(false)); + }; + + fetchDishes(); + }, []); + + return { dishes, isLoading, error }; +}; \ No newline at end of file diff --git a/frontend/archive/src/hooks/useFetchUsers.ts b/frontend/archive/src/hooks/useFetchUsers.ts new file mode 100644 index 0000000..f6df05b --- /dev/null +++ b/frontend/archive/src/hooks/useFetchUsers.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from "react"; +import {UserType} from "@/types/UserType"; +import {listUsers} from "@/utils/api/usersApi"; + +export const useFetchUsers = () => { + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchUsers = async () => { + listUsers() + .then((users: UserType[]) => setUsers(users)) + .catch((err) => setError((err as Error).message || "An error occurred.")) + .finally(() => setIsLoading(false)); + }; + + fetchUsers(); + }, []); + + return { users, isLoading, error }; +}; \ No newline at end of file diff --git a/frontend/archive/src/hooks/useRoutes.ts b/frontend/archive/src/hooks/useRoutes.ts new file mode 100644 index 0000000..7120da4 --- /dev/null +++ b/frontend/archive/src/hooks/useRoutes.ts @@ -0,0 +1,32 @@ +import {DishType} from "@/types/DishType"; +import {UserType} from "@/types/UserType"; + +const useRoutes = () => { + return { + home: () => "/", + auth: { + login: () => "/login", + register: () => "/register", + }, + dish: { + index: () => "/dishes", + create: () => "/dishes/create", + edit: (dish: DishType) => `/dishes/${dish.id}/edit`, + delete: (dish: DishType) => `/dishes/${dish.id}/delete`, + }, + schedule: { + date: { + edit: (date: string) => `/schedule/${date}/edit` + }, + history: () => "/scheduled-user-dishes/history", + }, + user: { + index: () => "/users", + create: () => `/users/create`, + edit: (user: UserType) => `/users/${user.id}/edit`, + delete: (user: UserType) => `/users/${user.id}/delete`, + } + }; +}; + +export default useRoutes; \ No newline at end of file diff --git a/frontend/archive/src/styles/base/globals.css b/frontend/archive/src/styles/base/globals.css new file mode 100644 index 0000000..918cbfb --- /dev/null +++ b/frontend/archive/src/styles/base/globals.css @@ -0,0 +1,19 @@ +html, body { + margin: 0; + padding: 0; + width: 100%; + overflow-x: hidden; +} + +body { + font-family: Arial, Helvetica, sans-serif; +} + + +.toggle-input:checked { + background-color: #22c55e; /* bg-green-500 */ +} + +.toggle-input:checked ~ span:last-child { + --tw-translate-x: 1.75rem; /* translate-x-7 */ +} \ No newline at end of file diff --git a/frontend/archive/src/styles/components/buttons.css b/frontend/archive/src/styles/components/buttons.css new file mode 100644 index 0000000..07fc4de --- /dev/null +++ b/frontend/archive/src/styles/components/buttons.css @@ -0,0 +1,42 @@ +.button-primary-solid { + background-color: var(--color-primary); + color: var(--color-secondary-200); + border: 1px solid var(--color-primary); + text-transform: uppercase; + font-family: "Anta", serif; + font-style: normal; + font-size: 1.1rem; + font-weight: 600; + padding: 4px 16px 2px 16px; +} +.button-primary-outline { + background-color: var(--color-background); + color: var(--color-primary); + border: 1px solid var(--color-primary); + text-transform: uppercase; + font-family: "Anta", serif; + font-style: normal; + font-size: 1.1rem; + font-weight: 600; + padding: 4px 16px 2px 16px; +} + +.button-secondary-solid { + background-color: var(--color-secondary); + color: var(--color-primary); + border: 1px solid var(--color-secondary); +} + +.button-accent-solid { + background-color: var(--color-accent-blue); + color: var(--color-secondary-900); + border: 1px solid var(--color-accent-blue); +} +.button-accent-outline { + background-color: var(--color-background); + color: var(--color-accent-blue); + border: 1px solid var(--color-accent-blue); +} +.button-accent-outline:hover { + background-color: var(--color-background-400); +} \ No newline at end of file diff --git a/frontend/archive/src/styles/components/select.css b/frontend/archive/src/styles/components/select.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/archive/src/styles/main.css b/frontend/archive/src/styles/main.css new file mode 100644 index 0000000..5ca69ea --- /dev/null +++ b/frontend/archive/src/styles/main.css @@ -0,0 +1,10 @@ +@import "./theme/borders.css"; +@import "./theme/fonts.css"; +@import "./components/buttons.css"; + +@import "./base/globals.css"; +@import "./theme/colors.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/archive/src/styles/theme/borders.css b/frontend/archive/src/styles/theme/borders.css new file mode 100644 index 0000000..b4ed5db --- /dev/null +++ b/frontend/archive/src/styles/theme/borders.css @@ -0,0 +1,14 @@ +.border-primary { + border-color: var(--color-primary); +} + +.border-secondary { + border-color: var(--color-secondary); +} + +.border-accent-blue { + border-color: var(--color-accent-blue); +} +.border-accent-800 { + border-color: var(--color-accent-blue-800); +} \ No newline at end of file diff --git a/frontend/archive/src/styles/theme/colors.css b/frontend/archive/src/styles/theme/colors.css new file mode 100644 index 0000000..338bcd7 --- /dev/null +++ b/frontend/archive/src/styles/theme/colors.css @@ -0,0 +1,10 @@ +@import './colors/root.css'; +@import 'colors/background.css'; +@import 'colors/border.css'; +@import 'colors/text.css'; + +body { + color: var(--color-secondary) !important; + background: var(--color-gray-600) !important; +} + diff --git a/frontend/archive/src/styles/theme/colors/background.css b/frontend/archive/src/styles/theme/colors/background.css new file mode 100644 index 0000000..e8548f5 --- /dev/null +++ b/frontend/archive/src/styles/theme/colors/background.css @@ -0,0 +1,226 @@ +.bg-gray-100 { + background-color: var(--color-gray-100) !important; +} +.bg-gray-200 { + background-color: var(--color-gray-200) !important; +} +.bg-gray-300 { + background-color: var(--color-gray-300) !important; +} +.bg-gray-400 { + background-color: var(--color-gray-400) !important; +} +.bg-gray-500 { + background-color: var(--color-gray-500) !important; +} +.bg-gray-600 { + background-color: var(--color-gray-600) !important; +} +.bg-gray-700 { + background-color: var(--color-gray-700) !important; +} +.bg-gray-800 { + background-color: var(--color-gray-800) !important; +} +.bg-gray-900 { + background-color: var(--color-gray-900) !important; +} + + +.bg-primary { + background-color: var(--color-primary) !important; +} + + +.bg-accent-blue { + background-color: var(--color-accent-blue-500) !important; +} +.bg-accent-blue-100 { + background-color: var(--color-accent-blue-100) !important; +} +.bg-accent-blue-200 { + background-color: var(--color-accent-blue-200) !important; +} +.bg-accent-blue-300 { + background-color: var(--color-accent-blue-300) !important; +} +.bg-accent-blue-400 { + background-color: var(--color-accent-blue-400) !important; +} +.bg-accent-blue-500 { + background-color: var(--color-accent-blue-500) !important; +} +.bg-accent-blue-600 { + background-color: var(--color-accent-blue-600) !important; +} +.bg-accent-blue-700 { + background-color: var(--color-accent-blue-700) !important; +} +.bg-accent-blue-800 { + background-color: var(--color-accent-blue-800) !important; +} +.bg-accent-blue-900 { + background-color: var(--color-accent-blue-900) !important; +} + + +.bg-accent-yellow { + background-color: var(--color-accent-yellow) !important; +} + +.bg-accent-yellow-100 { + background-color: var(--color-accent-yellow-100) !important; +} + +.bg-accent-yellow-200 { + background-color: var(--color-accent-yellow-200) !important; +} + +.bg-accent-yellow-300 { + background-color: var(--color-accent-yellow-300) !important; +} + +.bg-accent-yellow-400 { + background-color: var(--color-accent-yellow-400) !important; +} + +.bg-accent-yellow-500 { + background-color: var(--color-accent-yellow-500) !important; +} + +.bg-accent-yellow-600 { + background-color: var(--color-accent-yellow-600) !important; +} + +.bg-accent-yellow-700 { + background-color: var(--color-accent-yellow-700) !important; +} + +.bg-accent-yellow-800 { + background-color: var(--color-accent-yellow-800) !important; +} + +.bg-accent-yellow-900 { + background-color: var(--color-accent-yellow-900) !important; +} + + +.bg-success { + background-color: var(--color-success) !important; +} + +.bg-success-100 { + background-color: var(--color-success-100) !important; +} + +.bg-success-200 { + background-color: var(--color-success-200) !important; +} + +.bg-success-300 { + background-color: var(--color-success-300) !important; +} + +.bg-success-400 { + background-color: var(--color-success-400) !important; +} + +.bg-success-500 { + background-color: var(--color-success-500) !important; +} + +.bg-success-600 { + background-color: var(--color-success-600) !important; +} + +.bg-success-700 { + background-color: var(--color-success-700) !important; +} + +.bg-success-800 { + background-color: var(--color-success-800) !important; +} + +.bg-success-900 { + background-color: var(--color-success-900) !important; +} + +.bg-warning { + background-color: var(--color-warning) !important; +} + +.bg-warning-100 { + background-color: var(--color-warning-100) !important; +} + +.bg-warning-200 { + background-color: var(--color-warning-200) !important; +} + +.bg-warning-300 { + background-color: var(--color-warning-300) !important; +} + +.bg-warning-400 { + background-color: var(--color-warning-400) !important; +} + +.bg-warning-500 { + background-color: var(--color-warning-500) !important; +} + +.bg-warning-600 { + background-color: var(--color-warning-600) !important; +} + +.bg-warning-700 { + background-color: var(--color-warning-700) !important; +} + +.bg-warning-800 { + background-color: var(--color-warning-800) !important; +} + +.bg-warning-900 { + background-color: var(--color-warning-900) !important; +} + +.bg-danger { + background-color: var(--color-danger) !important; +} + +.bg-danger-100 { + background-color: var(--color-danger-100) !important; +} + +.bg-danger-200 { + background-color: var(--color-danger-200) !important; +} + +.bg-danger-300 { + background-color: var(--color-danger-300) !important; +} + +.bg-danger-400 { + background-color: var(--color-danger-400) !important; +} + +.bg-danger-500 { + background-color: var(--color-danger-500) !important; +} + +.bg-danger-600 { + background-color: var(--color-danger-600) !important; +} + +.bg-danger-700 { + background-color: var(--color-danger-700) !important; +} + +.bg-danger-800 { + background-color: var(--color-danger-800) !important; +} + +.bg-danger-900 { + background-color: var(--color-danger-900) !important; +} \ No newline at end of file diff --git a/frontend/archive/src/styles/theme/colors/border.css b/frontend/archive/src/styles/theme/colors/border.css new file mode 100644 index 0000000..8975296 --- /dev/null +++ b/frontend/archive/src/styles/theme/colors/border.css @@ -0,0 +1,286 @@ +.border-primary { + border-color: var(--color-primary); +} + +.border-primary-100 { + border-color: var(--color-primary-100); +} + +.border-primary-200 { + border-color: var(--color-primary-200); +} + +.border-primary-300 { + border-color: var(--color-primary-300); +} + +.border-primary-400 { + border-color: var(--color-primary-400); +} + +.border-primary-500 { + border-color: var(--color-primary-500); +} + +.border-primary-600 { + border-color: var(--color-primary-600); +} + +.border-primary-700 { + border-color: var(--color-primary-700); +} + +.border-primary-800 { + border-color: var(--color-primary-800); +} + +.border-primary-900 { + border-color: var(--color-primary-900); +} + + +.border-secondary { + border-color: var(--color-secondary); +} + +.border-secondary-100 { + border-color: var(--color-secondary-100); +} + +.border-secondary-200 { + border-color: var(--color-secondary-200); +} + +.border-secondary-300 { + border-color: var(--color-secondary-300); +} + +.border-secondary-400 { + border-color: var(--color-secondary-400); +} + +.border-secondary-500 { + border-color: var(--color-secondary-500); +} + +.border-secondary-600 { + border-color: var(--color-secondary-600); +} + +.border-secondary-700 { + border-color: var(--color-secondary-700); +} + +.border-secondary-800 { + border-color: var(--color-secondary-800); +} + +.border-secondary-900 { + border-color: var(--color-secondary-900); +} + +.border-accent-blue { + border-color: var(--color-accent-blue); +} + +.border-accent-blue-100 { + border-color: var(--color-accent-blue-100); +} + +.border-accent-blue-200 { + border-color: var(--color-accent-blue-200); +} + +.border-accent-blue-300 { + border-color: var(--color-accent-blue-300); +} + +.border-accent-blue-400 { + border-color: var(--color-accent-blue-400); +} + +.border-accent-blue-500 { + border-color: var(--color-accent-blue-500); +} + +.border-accent-blue-600 { + border-color: var(--color-accent-blue-600); +} + +.border-accent-blue-700 { + border-color: var(--color-accent-blue-700); +} + +.border-accent-blue-800 { + border-color: var(--color-accent-blue-800); +} + +.border-accent-blue-900 { + border-color: var(--color-accent-blue-900); +} + + +.border-accent-yellow { + border-color: var(--color-accent-yellow); +} + +.border-accent-yellow-100 { + border-color: var(--color-accent-yellow-100); +} + +.border-accent-yellow-200 { + border-color: var(--color-accent-yellow-200); +} + +.border-accent-yellow-300 { + border-color: var(--color-accent-yellow-300); +} + +.border-accent-yellow-400 { + border-color: var(--color-accent-yellow-400); +} + +.border-accent-yellow-500 { + border-color: var(--color-accent-yellow-500); +} + +.border-accent-yellow-600 { + border-color: var(--color-accent-yellow-600); +} + +.border-accent-yellow-700 { + border-color: var(--color-accent-yellow-700); +} + +.border-accent-yellow-800 { + border-color: var(--color-accent-yellow-800); +} + +.border-accent-yellow-900 { + border-color: var(--color-accent-yellow-900); +} + + +.border-background { + border-color: var(--color-background) !important; +} + +.border-danger { + border-color: var(--color-danger); +} + +.border-danger-100 { + border-color: var(--color-danger-100); +} + +.border-danger-200 { + border-color: var(--color-danger-200); +} + +.border-danger-300 { + border-color: var(--color-danger-300); +} + +.border-danger-400 { + border-color: var(--color-danger-400); +} + +.border-danger-500 { + border-color: var(--color-danger-500); +} + +.border-danger-600 { + border-color: var(--color-danger-600); +} + +.border-danger-700 { + border-color: var(--color-danger-700); +} + +.border-danger-800 { + border-color: var(--color-danger-800); +} + +.border-danger-900 { + border-color: var(--color-danger-900); +} + +.border-success { + border-color: var(--color-success); +} + +.border-success-100 { + border-color: var(--color-success-100); +} + +.border-success-200 { + border-color: var(--color-success-200); +} + +.border-success-300 { + border-color: var(--color-success-300); +} + +.border-success-400 { + border-color: var(--color-success-400); +} + +.border-success-500 { + border-color: var(--color-success-500); +} + +.border-success-600 { + border-color: var(--color-success-600); +} + +.border-success-700 { + border-color: var(--color-success-700); +} + +.border-success-800 { + border-color: var(--color-success-800); +} + +.border-success-900 { + border-color: var(--color-success-900); +} + +.border-warning { + border-color: var(--color-warning); +} + +.border-warning-100 { + border-color: var(--color-warning-100); +} + +.border-warning-200 { + border-color: var(--color-warning-200); +} + +.border-warning-300 { + border-color: var(--color-warning-300); +} + +.border-warning-400 { + border-color: var(--color-warning-400); +} + +.border-warning-500 { + border-color: var(--color-warning-500); +} + +.border-warning-600 { + border-color: var(--color-warning-600); +} + +.border-warning-700 { + border-color: var(--color-warning-700); +} + +.border-warning-800 { + border-color: var(--color-warning-800); +} + +.border-warning-900 { + border-color: var(--color-warning-900); +} \ No newline at end of file diff --git a/frontend/archive/src/styles/theme/colors/root.css b/frontend/archive/src/styles/theme/colors/root.css new file mode 100644 index 0000000..acffac0 --- /dev/null +++ b/frontend/archive/src/styles/theme/colors/root.css @@ -0,0 +1,193 @@ +:root { + --color-rose-50: #FFF5FC; + --color-rose-100: #FCE6F5; + --color-rose-200: #FAC3E7; + --color-rose-300: #F7A1D5; + --color-rose-400: #F25EAB; + --color-rose-500: #ED1F79; + --color-rose-600: #D61A68; + --color-rose-700: #B3124F; + --color-rose-800: #8F0B39; + --color-rose-900: #6B0626; + --color-rose-950: #450315; + + --color-deluge-50: #FAF7FC; + --color-deluge-100: #F2EDF7; + --color-deluge-200: #E2DAF0; + --color-deluge-300: #CEC3E6; + --color-deluge-400: #A49BD1; + --color-deluge-500: #7776BC; + --color-deluge-600: #6361AB; + --color-deluge-700: #43428C; + --color-deluge-800: #2C2B70; + --color-deluge-900: #191854; + --color-deluge-950: #0A0A36; + + --color-malibu-50: #FAFEFF; + --color-malibu-100: #F5FDFF; + --color-malibu-200: #E1F6FC; + --color-malibu-300: #CDEDFA; + --color-malibu-400: #ABDEF7; + --color-malibu-500: #85C7F2; + --color-malibu-600: #6EACDB; + --color-malibu-700: #4A81B5; + --color-malibu-800: #305F91; + --color-malibu-900: #1B3F6E; + --color-malibu-950: #0B2247; + + --color-gamboge-50: #FFFDF2; + --color-gamboge-100: #FCF7E3; + --color-gamboge-200: #FAECBB; + --color-gamboge-300: #F5DC93; + --color-gamboge-400: #EDBB47; + --color-gamboge-500: #E59500; + --color-gamboge-600: #CF7F00; + --color-gamboge-700: #AB6100; + --color-gamboge-800: #8A4700; + --color-gamboge-900: #663000; + --color-gamboge-950: #421C00; + + --color-ebony-clay-100: #9AA2B3; /* Soft slate */ + --color-ebony-clay-200: #7A8093; /* Balanced midtone */ + --color-ebony-clay-300: #5D637A; /* Former 400 */ + --color-ebony-clay-400: #444760; /* New shadowed steel */ + --color-ebony-clay-500: #2B2C41; + --color-ebony-clay-600: #24263C; /* Adjusted — less jumpy */ + --color-ebony-clay-700: #1D1E36; /* Interpolated midpoint */ + --color-ebony-clay-800: #131427; /* Slightly lifted from old 800 */ + --color-ebony-clay-900: #0A0B1C; + --color-ebony-clay-950: #030412; + + --color-alizarin-crimson-50: #FFF5FA; + --color-alizarin-crimson-100: #FCE6F1; + --color-alizarin-crimson-200: #FAC3DC; + --color-alizarin-crimson-300: #F59FC0; + --color-alizarin-crimson-400: #F05D82; + --color-alizarin-crimson-500: #E71D36; + --color-alizarin-crimson-600: #D1192F; + --color-alizarin-crimson-700: #AD1121; + --color-alizarin-crimson-800: #8C0B18; + --color-alizarin-crimson-900: #69060E; + --color-alizarin-crimson-950: #420308; + + --color-spring-green-50: #F5FFFC; + --color-spring-green-100: #E8FFF9; + --color-spring-green-200: #C7FFEE; + --color-spring-green-300: #A4FCDF; + --color-spring-green-400: #62FCBC; + --color-spring-green-500: #21FA90; + --color-spring-green-600: #1BE07A; + --color-spring-green-700: #13BA5E; + --color-spring-green-800: #0C9646; + --color-spring-green-900: #07702D; + --color-spring-green-950: #03471A; + + --color-burning-orange-50: #FFFBF5; + --color-burning-orange-100: #FFF7EB; + --color-burning-orange-200: #FFE8CC; + --color-burning-orange-300: #FFD5AD; + --color-burning-orange-400: #FFA973; + --color-burning-orange-500: #FF6B35; + --color-burning-orange-600: #E65A2C; + --color-burning-orange-700: #BF441F; + --color-burning-orange-800: #993114; + --color-burning-orange-900: #731F0A; + --color-burning-orange-950: #4A1004; + + /* Standard naming */ + + --color-primary: var(--color-rose-500); + --color-primary-100: var(--color-rose-100); + --color-primary-200: var(--color-rose-200); + --color-primary-300: var(--color-rose-300); + --color-primary-400: var(--color-rose-400); + --color-primary-500: var(--color-rose-500); + --color-primary-600: var(--color-rose-600); + --color-primary-700: var(--color-rose-700); + --color-primary-800: var(--color-rose-800); + --color-primary-900: var(--color-rose-900); + + --color-secondary: var(--color-deluge-500); + --color-secondary-100: var(--color-deluge-100); + --color-secondary-200: var(--color-deluge-200); + --color-secondary-300: var(--color-deluge-300); + --color-secondary-400: var(--color-deluge-400); + --color-secondary-500: var(--color-deluge-500); + --color-secondary-600: var(--color-deluge-600); + --color-secondary-700: var(--color-deluge-700); + --color-secondary-800: var(--color-deluge-800); + --color-secondary-900: var(--color-deluge-900); + + --color-accent-blue: var(--color-malibu-500); + --color-accent-blue-100: var(--color-malibu-100); + --color-accent-blue-200: var(--color-malibu-200); + --color-accent-blue-300: var(--color-malibu-300); + --color-accent-blue-400: var(--color-malibu-400); + --color-accent-blue-500: var(--color-malibu-500); + --color-accent-blue-600: var(--color-malibu-600); + --color-accent-blue-700: var(--color-malibu-700); + --color-accent-blue-800: var(--color-malibu-800); + --color-accent-blue-900: var(--color-malibu-900); + + --color-accent-yellow: var(--color-gamboge-500); + --color-accent-yellow-50: var(--color-gamboge-50); + --color-accent-yellow-100: var(--color-gamboge-100); + --color-accent-yellow-200: var(--color-gamboge-200); + --color-accent-yellow-300: var(--color-gamboge-300); + --color-accent-yellow-400: var(--color-gamboge-400); + --color-accent-yellow-500: var(--color-gamboge-500); + --color-accent-yellow-600: var(--color-gamboge-600); + --color-accent-yellow-700: var(--color-gamboge-700); + --color-accent-yellow-800: var(--color-gamboge-800); + --color-accent-yellow-900: var(--color-gamboge-900); + --color-accent-yellow-950: var(--color-gamboge-950); + + --color-gray-100: var(--color-ebony-clay-100); + --color-gray-200: var(--color-ebony-clay-200); + --color-gray-300: var(--color-ebony-clay-300); + --color-gray-400: var(--color-ebony-clay-400); + --color-gray-500: var(--color-ebony-clay-500); + --color-gray-600: var(--color-ebony-clay-600); + --color-gray-700: var(--color-ebony-clay-700); + --color-gray-800: var(--color-ebony-clay-800); + --color-gray-900: var(--color-ebony-clay-900); + + --color-danger: var(--color-alizarin-crimson-500); + --color-danger-50: var(--color-alizarin-crimson-50); + --color-danger-100: var(--color-alizarin-crimson-100); + --color-danger-200: var(--color-alizarin-crimson-200); + --color-danger-300: var(--color-alizarin-crimson-300); + --color-danger-400: var(--color-alizarin-crimson-400); + --color-danger-500: var(--color-alizarin-crimson-500); + --color-danger-600: var(--color-alizarin-crimson-600); + --color-danger-700: var(--color-alizarin-crimson-700); + --color-danger-800: var(--color-alizarin-crimson-800); + --color-danger-900: var(--color-alizarin-crimson-900); + --color-danger-950: var(--color-alizarin-crimson-950); + + --color-success: var(--color-spring-green-500); + --color-success-50: var(--color-spring-green-50); + --color-success-100: var(--color-spring-green-100); + --color-success-200: var(--color-spring-green-200); + --color-success-300: var(--color-spring-green-300); + --color-success-400: var(--color-spring-green-400); + --color-success-500: var(--color-spring-green-500); + --color-success-600: var(--color-spring-green-600); + --color-success-700: var(--color-spring-green-700); + --color-success-800: var(--color-spring-green-800); + --color-success-900: var(--color-spring-green-900); + --color-success-950: var(--color-spring-green-950); + + --color-warning: var(--color-burning-orange-500); + --color-warning-50: var(--color-burning-orange-50); + --color-warning-100: var(--color-burning-orange-100); + --color-warning-200: var(--color-burning-orange-200); + --color-warning-300: var(--color-burning-orange-300); + --color-warning-400: var(--color-burning-orange-400); + --color-warning-500: var(--color-burning-orange-500); + --color-warning-600: var(--color-burning-orange-600); + --color-warning-700: var(--color-burning-orange-700); + --color-warning-800: var(--color-burning-orange-800); + --color-warning-900: var(--color-burning-orange-900); + --color-warning-950: var(--color-burning-orange-950); +} diff --git a/frontend/archive/src/styles/theme/colors/text.css b/frontend/archive/src/styles/theme/colors/text.css new file mode 100644 index 0000000..1683846 --- /dev/null +++ b/frontend/archive/src/styles/theme/colors/text.css @@ -0,0 +1,216 @@ +.text-primary { + color: var(--color-primary); +} +.text-primary-100 { + color: var(--color-primary-100); +} +.text-primary-200 { + color: var(--color-primary-200); +} +.text-primary-300 { + color: var(--color-primary-300); +} +.text-primary-400 { + color: var(--color-primary-400); +} +.text-primary-500 { + color: var(--color-primary-500); +} +.text-primary-600 { + color: var(--color-primary-600); +} +.text-primary-700 { + color: var(--color-primary-700); +} +.text-primary-800 { + color: var(--color-primary-800); +} +.text-primary-900 { + color: var(--color-primary-900); +} + +.text-secondary { + color: var(--color-secondary); +} +.text-secondary-100 { + color: var(--color-secondary-100); +} +.text-secondary-200 { + color: var(--color-secondary-200); +} +.text-secondary-300 { + color: var(--color-secondary-300); +} +.text-secondary-400 { + color: var(--color-secondary-400); +} +.text-secondary-500 { + color: var(--color-secondary-500); +} +.text-secondary-600 { + color: var(--color-secondary-600); +} +.text-secondary-700 { + color: var(--color-secondary-700); +} +.text-secondary-800 { + color: var(--color-secondary-800); +} +.text-secondary-900 { + color: var(--color-secondary-900); +} + +.text-accent-blue { + color: var(--color-accent-blue); +} +.text-accent-blue-100 { + color: var(--color-accent-blue-100); +} +.text-accent-blue-200 { + color: var(--color-accent-blue-200); +} +.text-accent-blue-300 { + color: var(--color-accent-blue-300); +} +.text-accent-blue-400 { + color: var(--color-accent-blue-400); +} +.text-accent-blue-500 { + color: var(--color-accent-blue-500); +} +.text-accent-blue-600 { + color: var(--color-accent-blue-600); +} +.text-accent-blue-700 { + color: var(--color-accent-blue-700); +} +.text-accent-blue-800 { + color: var(--color-accent-blue-800); +} +.text-accent-blue-900 { + color: var(--color-accent-blue-900); +} + +.text-accent-yellow { + color: var(--color-accent-yellow); +} +.text-accent-yellow-100 { + color: var(--color-accent-yellow-100); +} +.text-accent-yellow-200 { + color: var(--color-accent-yellow-200); +} +.text-accent-yellow-300 { + color: var(--color-accent-yellow-300); +} +.text-accent-yellow-400 { + color: var(--color-accent-yellow-400); +} +.text-accent-yellow-500 { + color: var(--color-accent-yellow-500); +} +.text-accent-yellow-600 { + color: var(--color-accent-yellow-600); +} +.text-accent-yellow-700 { + color: var(--color-accent-yellow-700); +} +.text-accent-yellow-800 { + color: var(--color-accent-yellow-800); +} +.text-accent-yellow-900 { + color: var(--color-accent-yellow-900); +} + +.text-danger { + color: var(--color-danger); +} +.text-danger-100 { + color: var(--color-danger-100); +} +.text-danger-200 { + color: var(--color-danger-200); +} +.text-danger-300 { + color: var(--color-danger-300); +} +.text-danger-400 { + color: var(--color-danger-400); +} +.text-danger-500 { + color: var(--color-danger-500); +} +.text-danger-600 { + color: var(--color-danger-600); +} +.text-danger-700 { + color: var(--color-danger-700); +} +.text-danger-800 { + color: var(--color-danger-800); +} +.text-danger-900 { + color: var(--color-danger-900); +} + +.text-warning { + color: var(--color-warning); +} +.text-warning-100 { + color: var(--color-warning-100); +} +.text-warning-200 { + color: var(--color-warning-200); +} +.text-warning-300 { + color: var(--color-warning-300); +} +.text-warning-400 { + color: var(--color-warning-400); +} +.text-warning-500 { + color: var(--color-warning-500); +} +.text-warning-600 { + color: var(--color-warning-600); +} +.text-warning-700 { + color: var(--color-warning-700); +} +.text-warning-800 { + color: var(--color-warning-800); +} +.text-warning-900 { + color: var(--color-warning-900); +} + +.text-success { + color: var(--color-success); +} +.text-success-100 { + color: var(--color-success-100); +} +.text-success-200 { + color: var(--color-success-200); +} +.text-success-300 { + color: var(--color-success-300); +} +.text-success-400 { + color: var(--color-success-400); +} +.text-success-500 { + color: var(--color-success-500); +} +.text-success-600 { + color: var(--color-success-600); +} +.text-success-700 { + color: var(--color-success-700); +} +.text-success-800 { + color: var(--color-success-800); +} +.text-success-900 { + color: var(--color-success-900); +} \ No newline at end of file diff --git a/frontend/archive/src/styles/theme/fonts.css b/frontend/archive/src/styles/theme/fonts.css new file mode 100644 index 0000000..2646418 --- /dev/null +++ b/frontend/archive/src/styles/theme/fonts.css @@ -0,0 +1,95 @@ +/* Import Space Grotesk from Google Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&family=Syncopate:wght@400;700&display=swap'); + +/* Global font settings */ + +/* Set Space Grotesk as the default font */ +body { + font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; + font-size: 14px; + line-height: 1.6; + color: #333; +} + +/* Use Anta for headings */ +h1, h2, h3 { + font-family: 'Syncopate', sans-serif; + color: #111; +} + +/* Use Space Grotesk for smaller text like paragraphs */ +p { + font-family: 'Space Grotesk', sans-serif; +} + + +.font-default { + font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; +} + +.font-syncopate { + font-family: "Syncopate", serif !important; +} + +.font-space-grotesk { + font-family: 'Space Grotesk', sans-serif; +} + +.font-weight-100 { + font-weight: 100; +} +.font-weight-200 { + font-weight: 200; +} +.font-weight-300 { + font-weight: 300; +} +.font-weight-400 { + font-weight: 400; +} +.font-weight-500 { + font-weight: 500; +} +.font-weight-600 { + font-weight: 600; +} +.font-weight-700 { + font-weight: 700; +} +.font-weight-800 { + font-weight: 800; +} +.font-weight-900 { + font-weight: 900; +} + +.font-size-12 { + font-size: 12px !important; +} + +.font-size-14 { + font-size: 14px !important; +} + +.font-size-16 { + font-size: 16px !important; +} + +.font-size-18 { + font-size: 18px !important; +} + +.font-size-20 { + font-size: 20px !important; +} + +.font-size-24 { + font-size: 24px !important; +} + +.font-size-32 { + font-size: 32px !important; +} +.font-size-48 { + font-size: 48px !important; +} \ No newline at end of file diff --git a/frontend/archive/src/types/DishType.ts b/frontend/archive/src/types/DishType.ts new file mode 100644 index 0000000..e9986c2 --- /dev/null +++ b/frontend/archive/src/types/DishType.ts @@ -0,0 +1,20 @@ +import {UserType} from "@/types/UserType"; + +export type DishType = { + id: number + name: string, + recurrence: number, + users: UserType[], +} + +export type DishDateType = { + id: number; + date: string; + dish: DishType; + user: UserType; +} + +export type ScheduledDishesType = { + date: string; + dishes: { dish: DishType, user: UserType }[]; +} diff --git a/frontend/archive/src/types/ScheduleType.ts b/frontend/archive/src/types/ScheduleType.ts new file mode 100644 index 0000000..9b03fcf --- /dev/null +++ b/frontend/archive/src/types/ScheduleType.ts @@ -0,0 +1,27 @@ +import { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType"; +import { UserType } from "@/types/UserType"; + +export type RecurrenceType = { + type: "App\\Models\\WeeklyRecurrence" | "App\\Models\\MinimumRecurrence"; + value: number; +} + +export type ScheduleType = { + id: number; + date: string; + scheduled_user_dishes: ScheduledUserDishType[]; + is_skipped: boolean; +} + +export type FilledScheduleType = { + id?: number; + date: string; + is_skipped?: boolean; + scheduled_user_dishes: ScheduledUserDishType[]; +} + +export type ScheduleDataType = { + user: UserType; + scheduled_user_dish: UserDishType | null; + user_dishes: UserDishType[]; +} \ No newline at end of file diff --git a/frontend/archive/src/types/ScheduledUserDishType.ts b/frontend/archive/src/types/ScheduledUserDishType.ts new file mode 100644 index 0000000..5ff7c94 --- /dev/null +++ b/frontend/archive/src/types/ScheduledUserDishType.ts @@ -0,0 +1,21 @@ +import {UserType} from "@/types/UserType"; +import {DishType} from "@/types/DishType"; +import {RecurrenceType} from "@/types/ScheduleType"; + +export type UserDishType = { + id: number; + dish: DishType; + user: UserType; + recurrences: RecurrenceType[]; +} + +export type UserDishWithoutUserType = { + id: number; + dish: DishType; + recurrences: RecurrenceType[]; +} + +export type ScheduledUserDishType = { + id: number; + user_dish: UserDishType; +} \ No newline at end of file diff --git a/frontend/archive/src/types/UserDishType.ts b/frontend/archive/src/types/UserDishType.ts new file mode 100644 index 0000000..32f9dfc --- /dev/null +++ b/frontend/archive/src/types/UserDishType.ts @@ -0,0 +1,8 @@ +import {UserType} from "@/types/UserType"; +import {RecurrenceType} from "@/types/ScheduleType"; + +export type DishType = { + user: UserType; + dish: DishType; + recurrences: RecurrenceType[]; +} \ No newline at end of file diff --git a/frontend/archive/src/types/UserType.ts b/frontend/archive/src/types/UserType.ts new file mode 100644 index 0000000..1129ff0 --- /dev/null +++ b/frontend/archive/src/types/UserType.ts @@ -0,0 +1,7 @@ +import {UserDishWithoutUserType} from "@/types/ScheduledUserDishType"; + +export type UserType = { + id: number; + name: string; + user_dishes: UserDishWithoutUserType[]; +}; \ No newline at end of file diff --git a/frontend/archive/src/utils/api/apiRequest.ts b/frontend/archive/src/utils/api/apiRequest.ts new file mode 100644 index 0000000..e0690fa --- /dev/null +++ b/frontend/archive/src/utils/api/apiRequest.ts @@ -0,0 +1,107 @@ +export const apiRequest = async (url: string, options: RequestInit = {}) => { + const token = localStorage.getItem('token'); + + const allowedRequests = [ + '/api/auth/login', + '/api/auth/register', + ] + + if (allowedRequests.includes(url)) { + return publicRequest(url, options) + } + + if (!token) { + throw new Error('No authentication token found.' + url); + } + + return privateRequest(url, token, options); +}; + +export const publicRequest = async (fullUrl: string, options: RequestInit = {}) => { + console.log('→ Sending request', fullUrl, options.method); + + const url = `${process.env.NEXT_PUBLIC_API_URL}${fullUrl}`; + + const response = await fetch(url, { + headers: { + ...(options.headers || {}), + }, + ...options, + }); + + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); + } + + return response.json(); +} + +export const privateRequest = async (fullUrl: string, token: string, options: RequestInit = {}) => { + const headers = { + ...(options.headers || {}), + Authorization: `Bearer ${token}`, + }; + + const url = `${process.env.NEXT_PUBLIC_API_URL}${fullUrl}`; + + const response = await fetch(url, { headers, ...options }); + + // Authentication failure - token invalid - redirect to login + if (response.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); + + window.location.href = '/login'; + + throw new Error('Unauthorized. Redirecting to login.'); + } + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); + } + + return response.json(); +} + + +// Add shorthand HTTP methods +apiRequest.get = (url: string, options: RequestInit = {}) => { + return apiRequest(url, { ...options, method: 'GET' }); +}; + +apiRequest.post = | undefined>( + url: string, + body: TBody, + options: RequestInit = {} +) => { + return apiRequest(url, { + ...options, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); +}; + +apiRequest.put = | undefined>( + url: string, + body: TBody, + options: RequestInit = {} +) => { + return apiRequest(url, { + ...options, + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); +}; + +apiRequest.delete = (url: string, options: RequestInit = {}) => { + return apiRequest(url, { ...options, method: 'DELETE' }); +}; \ No newline at end of file diff --git a/frontend/archive/src/utils/api/auth.ts b/frontend/archive/src/utils/api/auth.ts new file mode 100644 index 0000000..f4511aa --- /dev/null +++ b/frontend/archive/src/utils/api/auth.ts @@ -0,0 +1,28 @@ +import { apiRequest } from '@/utils/api/apiRequest'; + +export const login = async (email: string, password: string) => { + const data = await apiRequest.post('/api/auth/login', { email, password }); + + if (!data.access_token) { + throw new Error('No access token returned from login.'); + } + + localStorage.setItem('token', data.access_token); + + return data; +}; + + +export const register = async (name: string, email: string, password: string, passwordConfirmation: string) => { + const data = await apiRequest.post('/api/auth/register', { + name, + email, + password, + password_confirmation: passwordConfirmation, // Match the backend's expected parameter + }); + + // Store the token (if returned by the backend) similarly to login + localStorage.setItem('token', data.access_token); + + return data; +}; diff --git a/frontend/archive/src/utils/api/dishApi.ts b/frontend/archive/src/utils/api/dishApi.ts new file mode 100644 index 0000000..8d38dba --- /dev/null +++ b/frontend/archive/src/utils/api/dishApi.ts @@ -0,0 +1,149 @@ +import {DishType} from "@/types/DishType"; +import {apiRequest} from "@/utils/api/apiRequest"; + +export const listDishes = async (): Promise => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(`/api/dishes`, { + headers: { + Authorization: `Bearer ${token}`, + }}) + .then((data) => { + if (data?.payload?.dishes) { + return data.payload.dishes as DishType[]; + } + throw new Error('SOMETHING WENT WRONG'); + }) + .catch((error) => { + throw error; + }); +}; + +export const fetchDish = async (id: number): Promise => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(`/api/dishes/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }}) + .then((data) => { + if (data?.payload?.dish) { + return data.payload.dish as DishType; + } + throw new Error('SOMETHING WENT WRONG'); + }) + .catch((error) => { + throw error; + }); +}; + +export const createDish = async ( + name: string, + // recurrence: number, + // userIds: number[] +) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.post(`/api/dishes`, { + name, + // recurrence, + // users: userIds, + }, { + headers: { + Authorization: `Bearer ${token}`, + }, + }).catch(() => { + throw new Error("Failed to create dish. Please try again later."); + }); +}; + +export const updateDish = async (dish_id: number, name: string) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.put(`/api/dishes/${dish_id}`, {name}, { + headers: { + Authorization: `Bearer ${token}`, + }}) + .catch((error) => { + throw error; + }); +}; + +export const deleteDish = async (id: number) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.delete(`/api/dishes/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }}) + .then((data) => { + return data; + }) + .catch((error) => { + throw error; + }); +}; + +export const addUserToDish = async (dish_id: number, user_id: number) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.post(`/api/dishes/${dish_id}/users/add`, { + users: [user_id], + }, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((data) => { + return data; + }) + .catch((error) => { + throw error; + }); +}; + +export const removeUserFromDish = async (dish_id: number, user_id: number) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.post(`/api/dishes/${dish_id}/users/remove`, { + users: [user_id], + }, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((data) => { + return data; + }) + .catch((error) => { + throw error; + }); +}; diff --git a/frontend/archive/src/utils/api/scheduleApi.ts b/frontend/archive/src/utils/api/scheduleApi.ts new file mode 100644 index 0000000..d7fcbf3 --- /dev/null +++ b/frontend/archive/src/utils/api/scheduleApi.ts @@ -0,0 +1,112 @@ +import { apiRequest } from "@/utils/api/apiRequest"; +import { isValidDate } from "@/helpers/Date"; + +export const listSchedule = async (startDate?: string, endDate?: string) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + if (startDate && !isValidDate(startDate)) { + throw new Error('Invalid start date'); + } + if (endDate && !isValidDate(endDate)) { + throw new Error('Invalid end date'); + } + + const params = new URLSearchParams(); + if (startDate) params.append('start', startDate); + if (endDate) params.append('end', endDate); + + const endpoint = `/api/schedule${params.toString() ? `?${params.toString()}` : ''}`; + + return apiRequest.get(endpoint, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduleData) => scheduleData?.payload?.schedule); +}; + +export const getScheduleForDate = async (date: string) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + if (!isValidDate(date)) { + throw new Error('Invalid date'); + } + + const endpoint = `/api/schedule/${date}`; + + return apiRequest.get(endpoint, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduleData) => scheduleData?.payload?.schedule) + .catch((err) => { + throw err; + }); +}; + +// Update the schedule for a specific date (e.g., mark as skipped) +export const updateScheduleForDate = async (date: string, isSkipped: boolean) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + if (!isValidDate(date)) { + throw new Error('Invalid date'); + } + + const endpoint = `/api/schedule/${date}`; + + return apiRequest.put(endpoint, { is_skipped: isSkipped }, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduleData) => scheduleData?.payload?.schedule) + .catch((err) => { + throw err; + }); +}; + +// Generate a new schedule (optional: overwrite the existing one) +export const generateSchedule = async (overwrite: boolean) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + const endpoint = `/api/schedule/generate`; + + return apiRequest.post(endpoint, { overwrite }, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduleData) => scheduleData?.payload?.schedule) + .catch((err) => { + throw err; + }); +}; + +export const scheduleUserDish = async (date: string, user_id: number, user_dish_id: number|null, skipped: boolean = false) => { + const token = localStorage.getItem('token'); + + if (!token) throw new Error('No token found in localStorage.'); + + const endpoint = `/api/schedule/${date}/user-dishes`; + + return apiRequest.post(endpoint, { user_dish_id, user_id, skipped }, { headers: { Authorization: `Bearer ${token}`} }) + .then((scheduleData) => scheduleData?.payload?.schedule) + .catch((err) => { throw err }); +}; \ No newline at end of file diff --git a/frontend/archive/src/utils/api/scheduledUserDishesApi.ts b/frontend/archive/src/utils/api/scheduledUserDishesApi.ts new file mode 100644 index 0000000..8a9b66c --- /dev/null +++ b/frontend/archive/src/utils/api/scheduledUserDishesApi.ts @@ -0,0 +1,77 @@ +import { apiRequest } from "@/utils/api/apiRequest"; + +export const listScheduledUserDishesStartingFromDate = async (startDate: string) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(`/api/scheduled-user-dishes?start=${startDate}`, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduledDishesData) => scheduledDishesData?.payload?.schedule) + .catch((err) => { + throw err; + }); +}; + +export const listScheduledUserDishesEndingAtDate = async (endDate: string) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(`/api/scheduled-user-dishes?end=${endDate}`, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduledDishesData) => scheduledDishesData?.payload?.schedule) + .catch((err) => { + throw err; + }); +}; + +export const getScheduledUserDish = async (id: number) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(`/api/scheduled-user-dishes/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduledDishesData) => scheduledDishesData?.payload?.scheduled_user_dish) + .catch((err) => { + throw err; + }); +}; + +export const updateScheduledUserDish = async (id: number, userDishId: number) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + const payload = userDishId > 0 + ? { user_dish_id: userDishId } + : { user_dish_id: null, is_skipped: true }; + + return apiRequest.put(`/api/scheduled-user-dishes/${id}`, payload, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((scheduledDishesData) => scheduledDishesData?.payload?.scheduled_user_dish) + .catch((err) => { + throw err; + }); +}; \ No newline at end of file diff --git a/frontend/archive/src/utils/api/userDishApi.ts b/frontend/archive/src/utils/api/userDishApi.ts new file mode 100644 index 0000000..39ba27f --- /dev/null +++ b/frontend/archive/src/utils/api/userDishApi.ts @@ -0,0 +1,18 @@ +import { apiRequest } from "@/utils/api/apiRequest"; +import { UserDishType } from "@/types/ScheduledUserDishType"; + +export const listUserDishes = async (): Promise => { + const token = localStorage.getItem('token'); + + if (!token) throw new Error('No token found in localStorage.'); + + return apiRequest.get(`/api/user-dishes`, { headers: { Authorization: `Bearer ${ token }` } }) + .then((data) => { + if (data?.payload?.user_dishes) return data.payload.user_dishes as UserDishType[]; + + throw new Error('SOMETHING WENT WRONG'); + }) + .catch((error) => { + throw error; + }); +}; diff --git a/frontend/archive/src/utils/api/usersApi.ts b/frontend/archive/src/utils/api/usersApi.ts new file mode 100644 index 0000000..8d70ed8 --- /dev/null +++ b/frontend/archive/src/utils/api/usersApi.ts @@ -0,0 +1,139 @@ +import {RecurrenceType} from "@/types/ScheduleType"; +import {apiRequest} from "@/utils/api/apiRequest"; +import {UserType} from "@/types/UserType"; + +export const listUsers = async () => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(`/api/users`, { + headers: { + Authorization: `Bearer ${token}`, + }}) + .then((data) => { + if (data?.payload?.users) { + return data.payload.users; + } + throw new Error('Failed to fetch users'); + }) + .catch((err) => { + throw err; + }); +}; + +export const showUser = async (userId: number) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(`/api/users/${userId}`, { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((data) => { + if (data?.payload?.user) { + return data.payload.user; + } + throw new Error('Failed to fetch users'); + }) + .catch((err) => { + throw err; + }); +}; + +export const createUser = async (name: string) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return await apiRequest.post('/api/users', {name}, { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); +}; + +export const updateUser = async (user: UserType, name: string) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return await apiRequest.put(`/api/users/${user.id}`, { name }, { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); +}; + +export const deleteUser = async (user: UserType) => { + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return await apiRequest.delete(`/api/users/${user.id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); +}; + + +export const getUserDishForUserAndDish = async (userId: number, dishId: number) => { + const endpoint = `/api/users/${userId}/dishes/${dishId}`; + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.get(endpoint, { + headers: { + Authorization: `Bearer ${token}`, + }}) + .then((data) => { + if (data?.payload?.user_dish) { + return data.payload.user_dish; + } + throw new Error('Failed to fetch user dish'); + }) + .catch((err) => { + throw err; + }); +}; + +export const syncUserDishRecurrences = async ( + dish_id: number, + user_id: number, + recurrenceData: RecurrenceType[] +) => { + const url = `/api/users/${user_id}/dishes/${dish_id}/recurrences`; + const token = localStorage.getItem('token'); + + if (!token) { + throw new Error('No token found in localStorage.'); + } + + return apiRequest.post(url, { recurrences: recurrenceData }, { + headers: { + Authorization: `Bearer ${token}`, + }, + }).catch((err) => { + throw err; + }); +}; + diff --git a/frontend/archive/src/utils/dateBuilder.ts b/frontend/archive/src/utils/dateBuilder.ts new file mode 100644 index 0000000..e41ce15 --- /dev/null +++ b/frontend/archive/src/utils/dateBuilder.ts @@ -0,0 +1,24 @@ +import { DateTime } from "luxon"; + +const transformDate = (inputDate: string, addSuffix = false): string => { + const date = DateTime.fromISO(inputDate) + const day = date.day + const suffix = addSuffix ? getDaySuffix(day) : '' + return date.toFormat("MMMM") + ` ${ day }${ suffix }, ` + date.toFormat("yyyy"); +} + +const getDaySuffix = (day: number): string => { + if (day >= 11 && day <= 13) return "th"; + switch (day % 10) { + case 1: + return "st"; + case 2: + return "nd"; + case 3: + return "rd"; + default: + return "th"; + } +}; + +export default transformDate; \ No newline at end of file diff --git a/frontend/archive/src/utils/scheduleBuilder.ts b/frontend/archive/src/utils/scheduleBuilder.ts new file mode 100644 index 0000000..8bbf15f --- /dev/null +++ b/frontend/archive/src/utils/scheduleBuilder.ts @@ -0,0 +1,19 @@ +import { ScheduleDataType, ScheduleType } from "@/types/ScheduleType"; +import { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType"; +import { UserType } from "@/types/UserType"; + +const ScheduleBuilder = ( + schedule: ScheduleType, + users: UserType[], + userDishes: UserDishType[] +): ScheduleDataType[] => users.map(user => { + return { + user, + scheduled_user_dish: schedule.scheduled_user_dishes + .filter((scheduledUserDish: ScheduledUserDishType) => scheduledUserDish.user_dish?.user.id === user.id) + .shift()?.user_dish ?? null, + user_dishes: userDishes.filter((userDish: UserDishType) => userDish.user.id === user.id) + } +}) + +export default ScheduleBuilder \ No newline at end of file diff --git a/frontend/archive/tailwind.config.ts b/frontend/archive/tailwind.config.ts new file mode 100644 index 0000000..109807b --- /dev/null +++ b/frontend/archive/tailwind.config.ts @@ -0,0 +1,18 @@ +import type { Config } from "tailwindcss"; + +export default { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/frontend/archive/tsconfig.json b/frontend/archive/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/frontend/archive/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ca4715b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4828 @@ +{ + "name": "my-react-router-app", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-react-router-app", + "dependencies": { + "@react-router/node": "^7.5.3", + "@react-router/serve": "^7.5.3", + "isbot": "^5.1.27", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.5.3" + }, + "devDependencies": { + "@react-router/dev": "^7.5.3", + "@tailwindcss/vite": "^4.1.4", + "@types/node": "^20", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "tailwindcss": "^4.1.4", + "typescript": "^5.8.3", + "vite": "^6.3.3", + "vite-tsconfig-paths": "^5.1.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", + "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mjackson/node-fetch-server": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==", + "license": "MIT" + }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-router/dev": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.6.0.tgz", + "integrity": "sha512-XSxEslex0ddJPxNNgdU1Eqmc9lsY/lhcLNCcRLAtlrOPyOz3Y8kIPpAf5T/U2AG3HGXFVBa9f8aQ7wXU3wTJSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.8", + "@babel/generator": "^7.21.5", + "@babel/parser": "^7.21.8", + "@babel/plugin-syntax-decorators": "^7.22.10", + "@babel/plugin-syntax-jsx": "^7.21.4", + "@babel/preset-typescript": "^7.21.5", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.22.5", + "@npmcli/package-json": "^4.0.1", + "@react-router/node": "7.6.0", + "arg": "^5.0.1", + "babel-dead-code-elimination": "^1.0.6", + "chokidar": "^4.0.0", + "dedent": "^1.5.3", + "es-module-lexer": "^1.3.1", + "exit-hook": "2.2.1", + "fs-extra": "^10.0.0", + "jsesc": "3.0.2", + "lodash": "^4.17.21", + "pathe": "^1.1.2", + "picocolors": "^1.1.1", + "prettier": "^2.7.1", + "react-refresh": "^0.14.0", + "semver": "^7.3.7", + "set-cookie-parser": "^2.6.0", + "valibot": "^0.41.0", + "vite-node": "3.0.0-beta.2" + }, + "bin": { + "react-router": "bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-router/serve": "^7.6.0", + "react-router": "^7.6.0", + "typescript": "^5.1.0", + "vite": "^5.1.0 || ^6.0.0", + "wrangler": "^3.28.2 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@react-router/serve": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wrangler": { + "optional": true + } + } + }, + "node_modules/@react-router/express": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.6.0.tgz", + "integrity": "sha512-nxSTCcTsVx94bXOI9JjG7Cg338myi8EdQWTOjA97v2ApX35wZm/ZDYos5MbrvZiMi0aB4KgAD62o4byNqF9Z1A==", + "license": "MIT", + "dependencies": { + "@react-router/node": "7.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "express": "^4.17.1 || ^5", + "react-router": "7.6.0", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/node": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.6.0.tgz", + "integrity": "sha512-agjDPUzisLdGJ7Q2lx/Z3OfdS2t1k6qv/nTvA45iahGsQJCMDvMqVoIi7iIULKQJwrn4HWjM9jqEp75+WsMOXg==", + "license": "MIT", + "dependencies": { + "@mjackson/node-fetch-server": "^0.2.0", + "source-map-support": "^0.5.21", + "stream-slice": "^0.1.2", + "undici": "^6.19.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.6.0", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/serve": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.6.0.tgz", + "integrity": "sha512-2O8ALEYgJfimvEdNRqMpnZb2N+DQ5UK/SKo9Xo3mTkt3no0rNTcNxzmhzD2tm92Q/HI7kHmMY1nBegNB2i1abA==", + "license": "MIT", + "dependencies": { + "@react-router/express": "7.6.0", + "@react-router/node": "7.6.0", + "compression": "^1.7.4", + "express": "^4.19.2", + "get-port": "5.1.1", + "morgan": "^1.10.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "react-router-serve": "bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.6.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", + "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.29.2", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", + "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-x64": "4.1.6", + "@tailwindcss/oxide-freebsd-x64": "4.1.6", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-x64-musl": "4.1.6", + "@tailwindcss/oxide-wasm32-wasi": "4.1.6", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", + "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", + "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", + "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", + "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", + "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", + "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", + "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", + "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", + "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", + "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.9", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", + "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", + "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz", + "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.6", + "@tailwindcss/oxide": "4.1.6", + "tailwindcss": "4.1.6" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", + "integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/react": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", + "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.4.tgz", + "integrity": "sha512-WxYAszDYgsMV31OVyoG4jbAgJI1Gw0Xq9V19zwhy6+hUUJlJIdZ3r/cbdmTqFv++SktQkZ/X+46yGFxp5XJBEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", + "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.152", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", + "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isbot": { + "version": "5.1.28", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.28.tgz", + "integrity": "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz", + "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rollup": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-slice": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz", + "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", + "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsconfck": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", + "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", + "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/valibot": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz", + "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.0-beta.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.0-beta.2.tgz", + "integrity": "sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1337f19 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "my-react-router-app", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@react-router/node": "^7.5.3", + "@react-router/serve": "^7.5.3", + "isbot": "^5.1.27", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.5.3" + }, + "devDependencies": { + "@react-router/dev": "^7.5.3", + "@tailwindcss/vite": "^4.1.4", + "@types/node": "^20", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "tailwindcss": "^4.1.4", + "typescript": "^5.8.3", + "vite": "^6.3.3", + "vite-tsconfig-paths": "^5.1.4" + } +} \ No newline at end of file diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5dbdfcddcb14182535f6d32d1c900681321b1aa3 GIT binary patch literal 15086 zcmeI33v3ic7{|AFEmuJ-;v>ep_G*NPi6KM`qNryCe1PIJ8siIN1WZ(7qVa)RVtmC% z)Ch?tN+afMKm;5@rvorJk zcXnoOc4q51HBQnQH_jn!cAg&XI1?PlX>Kl^k8qq0;zkha`kY$Fxt#=KNJAE9CMdpW zqr4#g8`nTw191(+H4xW8Tmyru2I^3=J1G3emPxkPXA=3{vvuvse_WWSshqaqls^-m zgB7q8&Vk*aYRe?sn$n53dGH#%3y%^vxv{pL*-h0Z4bmb_(k6{FL7HWIz(V*HT#IcS z-wE{)+0x1U!RUPt3gB97%p}@oHxF4|6S*+Yw=_tLtxZ~`S=z6J?O^AfU>7qOX`JNBbV&8+bO0%@fhQitKIJ^O^ zpgIa__qD_y07t@DFlBJ)8SP_#^j{6jpaXt{U%=dx!qu=4u7^21lWEYHPPY5U3TcoQ zX_7W+lvZi>TapNk_X>k-KO%MC9iZp>1E`N34gHKd9tK&){jq2~7OsJ>!G0FzxQFw6G zm&Vb(2#-T|rM|n3>uAsG_hnbvUKFf3#ay@u4uTzia~NY%XgCHfx4^To4BDU@)HlV? z@EN=g^ymETa1sQK{kRwyE4Ax8?wT&GvaG@ASO}{&a17&^v`y z!oPdiSiia^oov(Z)QhG2&|FgE{M9_4hJROGbnj>#$~ZF$-G^|zPj*QApltKe?;u;uKHJ~-V!=VLkg7Kgct)l7u39f@%VG8e3f$N-B zAu3a4%ZGf)r+jPAYCSLt73m_J3}p>}6Tx0j(wg4vvKhP!DzgiWANiE;Ppvp}P2W@m z-VbYn+NXFF?6ngef5CfY6ZwKnWvNV4z6s^~yMXw2i5mv}jC$6$46g?G|CPAu{W5qF zDobS=zb2ILX9D827g*NtGe5w;>frjanY{f)hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)# z)0C|?$o>jzh<|-cpf

K7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_ zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf` z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j literal 0 HcmV?d00001 diff --git a/frontend/react-router.config.ts b/frontend/react-router.config.ts new file mode 100644 index 0000000..6ff16f9 --- /dev/null +++ b/frontend/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..dc391a4 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": [ + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..4a88d58 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,8 @@ +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], +});